Здравствуйте! Данная статья является продолжением цикла статей, посвященных разработке для мобильной платформы Sailfish OS. На этот раз речь пойдёт об использовании Bluetooth для установки соединения между двумя устройствами и передачи данных.
Технология Bluetooth позволяет создавать беспроводное соединение, благодаря которому возможна передача абсолютно любых данных. Существует целый ряд задач для которых Bluetooth является общепринятым решением, например, передача файлов с одного устройства на другое, подключение к Bluetooth гарнитурам или дистанционное управление сканерами и принтерами.
Будем рассматривать использование технологии Bluetooth на примере реализации приложения для обмена строками. Назовём его bluetooth-messenger. Приложение будет работать в двух режимах: сервер и клиент. Сервер будет регистрировать Bluetooth сервис и реагировать на подключение клиента. Клиент же будет искать созданный сервис, подключаться к нему и передавать данные. Как следствие, необходимо наличие двух устройств, работающих под управлением Sailfish OS.
В итоге приложение будет работать следующим образом:
Реализация данного приложения позволит полностью осветить необходимые инструменты для установки связи между двумя устройствами и обмена данными между ними.
Для взаимодействия с Bluetooth приложению иногда требуются повышенные привилегии (для поиска сервисов, изменения настроек видимости, для сопряжения устройств). При отсутствии повышенных привилегий часть функционала будет недоступна, поэтому рекомендуем выдавать их приложению, работающему с Bluetooth.
Для отладки необходимо запускать приложение с помощью devel-su с флагом -p. Это позволяет запускать приложение с повышенными привилегиями, а отладочный вывод будет доступен в консоли.
Для того, чтобы запустить приложение с повышенными привилегиями по нажатию на иконку, необходимо сделать некоторые настройки в исходных файлах проекта. Во-первых, исполняемый файл приложения нужно запускать с помощью invoker. Invoker находит главную функцию приложения и запускает её с переданными ему аргументами. Это настраивается в .desktop файле проекта следующей строчкой:
Во-вторых, необходимо создать файл с названием, соответствующим названию исполняемого файла, в директории /usr/share/mapplauncherd/privileges.d/ и поместить туда строчку:
Запятая в конце строчки обязательно нужна. Таким образом при нажатии на иконку приложения, пользователь запустит его с повышенными привилегиями.
Для начала необходимо понять, каким образом возможно управлять состоянием Bluetooth. Для этого следует использовать систему D-Bus, взаимодействие с которой было описано в одной из предыдущих статей. С помощью данной системы мы имеем возможность включать и выключать питание Bluetooth и настраивать видимость для других устройств.
Для включения Bluetooth необходимо использовать сервис net.connman. На интерфейсе net.connman по пути /net/connman/technology/bluetooth есть метод SetProperty, с помощью которого возможно установить значение свойства Powered, которое отвечает за то включен Bluetooth или нет. Устанавливается свойство следующим образом:
Создаём экземпляр QDBusInterface с использованием перечисленных ранее сервиса, пути и интерфейса. Затем на интерфейсе вызываем метод SetProperty с двумя аргументами: названием свойства и значением.
После включения Bluetooth будет полезно настроить видимость для других устройств. Для этого используем сервис org.bluez. Во-первых необходимо получить путь, соответствующий текущему устройству. Для этого по корневому пути на интерфейсе org.bluez.Manager вызываем метод DefaultAdapter, содержащий в выходных аргументах путь к текущему адаптеру, который впоследствии мы будем использовать для установки видимости.
После получения пути для установки видимости необходимо использовать метод SetProperty на интерфейсе org.bluez.Adapter для установки следующих свойств:
Неограниченное по времени обнаружение включаем следующими строчками:
Следует заметить, что для настройки опций видимости необходимо, чтобы приложение было запущено с повышенными привилегиями.
Для начала нам необходимо создать сервер и зарегистрировать на нём сервис. Данный сервис будет принимать сообщения от клиентов и отвечать им. Для решения данной задачи мы создадим класс MessengerServer, заголовочный файл которого будет содержать следующее:
Теперь более детально рассмотрим компоненты и содержание методов данного класса.
Устройство может оповестить другие устройства, осуществляющие поиск по Bluetooth путём регистрации сервиса. Для этого используется класс QBluetoothServer. С его помощью можно создать Bluetooth сервер и зарегистрировать на нём сервис, который будет сообщать устройствам, что он из себя представляет.
QBluetoothServer содержит набор методов для установки сервера на устройстве и регистрации сервиса. В частности представляют интерес:
Остальные методы служат для остановки сервера, получения текущего адреса или порта, проверки статуса и другие. Они не настолько интересны, и о них вы можете почитать в официальной документации.
После того как мы подняли сервер, необходимо зарегистрировать сервис. Сервис представляет собой описание какой-либо службы, выполняющей определённые обязанности. Описывается сервис с помощью объекта QBluetoothServiceInfo путём установки атрибутов выделенными методами. Для решения поставленной выше задачи используем метод startServer():
Первой строчкой мы создаём сервер, который в качестве протокола использует RFCOMM. Затем соединяем сигнал о новом подключении со слотом нашего класса. После этого включаем прослушивание на нашем адресе, для чего создаём экземпляр текущего устройства, из которого извлекаем его адрес и передаём методу listen(). Таким образом мы устанавливаем сервер.
Регистрация сервиса требует большего количества кода для указания всех требующихся для его работы параметров:
Здесь устанавливаем название сервиса, описание, поставщика сервиса (например название компании) и уникальный идентификатор (в данном приложении содержится в константе в виде строки и задаётся в формате xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, где x – это шестнадцатеричное число). Первые три атрибута позволяют получить базовое представление о найденном сервисе в то время как четвёртый может использоваться устройствами для поиска конкретного сервиса.
Конструкция подобного рода использует последовательность (QBluetoothServiceInfo::Sequence) для установки прочих атрибутов. В данном случае мы устанавливаем уникальный идентификатор сервиса. Таким образом сервер даёт знать о том, какие сервисы он предоставляет.
Данными строчками устанавливаем группу общедоступного поиска, что позволит устройствам свободно находить данный сервис. В противном случае сервис найден не будет.
Здесь для доступа к сервису устанавливаем протокол RFCOMM, аналогично используемому сервером.
Наконец, выполняем регистрацию созданного сервиса на адресе, полученном ранее и используемом сервером. С этого момента сервис будет виден при поиске по Bluetooth другими устройствами.
Теперь, когда сервис зарегистрирован и приложение готово принимать входящие соединения, необходимо их обрабатывать. Как упоминалось ранее, сервер приложения должен принимать строку от клиента, разворачивать её и отправлять обратно.
При подключении клиента к серверу мы создаём сокет, представленный в виде экземпляра QBluetoothSocket, который можно получить вызвав метод nextPendingConnection() на экземпляре класса QBluetoothServer. У сокета есть целый набор сигналов, позволяющих отследить его состояние, наиболее полезными из которых являются:
Используем их для обработки входящих соединений. Ранее мы прикрепили сигнал newConnection() к слоту clientConnected(), рассмотрим его реализацию.
Объект QBluetoothSocket является наследником QIODevice, как следствие ему доступны методы для чтения линии, символа, выбранного количества символов и т.п. Методы для чтения (как и методы для записи) используют QByteArray, что позволяет передавать не только строки, но и любые другие данные в виде набора байтов. Таким образом возможна передача любых типов данных вне зависимости от содержимого.
В нашем примере для обработки входящих сообщений мы соединили сигнал readyRead() с методом readSocket(), код которого выглядит следующим образом:
Для чтения данных в виде массива байтов мы используем метод readLine(), после чего преобразуем считанную линию в строку, разворачиваем её и отправляем обратно с помощью метода write(), преобразовав обратно в массив байтов. Таким образом реализованный нами сервер способен получать строку от любого другого устройства по Bluetooth и возвращать её обратно в развёрнутом виде.
Теперь, когда сервер реализован, запущен и ждёт входящих соединений, необходимо к нему подключиться. Каким образом возможно найти устройство, которое предоставляет необходимый сервис? Во-первых, необходимо произвести поиск сервисов, доступных на видимых Bluetooth устройствах и только затем к нему подключаться.
Заголовочный файл клиента имеет следующее содержание:
Рассмотрим компоненты, которые необходимы для реализации поиска сервиса и отправки сообщений.
Для поиска сервисов библиотека Qt предоставляет класс QBluetoothServiceDiscoveryAgent. Он позволяет автоматически проверить все устройства на наличие определённого сервиса, который мы ищем по UUID. В дальнейшем, при нахождении сервиса объект данного класса инициирует соответствующий сигнал, с помощью которого мы можем обработать результат поиска. Следует заметить, что использование данного класса требует, чтобы приложение было запущено с повышенными привилегиями. Класс содержит следующие интересующие нас методы:
Для обработки результата полезны сигналы:
Для поиска нашего конкретного сервиса необходимо установить методом setUuidFilter() фильтр по UUID, который мы указывали при регистрации сервиса и методом start() начать поиск. После этого при обнаружении нашего сервиса будет инициирован сигнал serviceDiscovered(). Экземпляр QBluetoothServiceInfo содержит информацию о найденном сервисе (имя, UUID, информация об устройстве, на котором он зарегистрирован и т.п.). Экземпляр данного класса мы будем использовать для подключения к сервису, о чём будет упомянуто вдальнейшем.
Конкретно в нашем примере будем рассматривать другой класс, не требующий повышенных привилегий – QBluetoothDeviceDiscoveryAgent. С его помощью возможен поиск устройств, а не сервисов, и он не требует повышенных привилегий. Для каждого найденного устройства будем просматривать сервисы, зарегистрированные на устройстве, и если в списке есть наш сервис, то считаем сервис найденным и в дальнейшем будем подключаться к нему.
QBluetoothDeviceDiscoveryAgent состоит из небольшого числа методов для поиска устройств. Наиболее полезными являются следующие:
Также в случае нахождения устройства будет немедленно инициирован сигнал deviceDiscovered(const QBluetoothDeviceInfo &info), который может служить для обработки результата.
Информация о найденных устройствах представлена в виде объекта QDeviceInfo. Из данного объекта можно извлечь данные с помощью специальных методов. Наиболее интересными являются следующие:
Теперь, когда мы знаем как искать сервисы на Bluetooth устройствах попробуем найти наш собственный сервис. В конструкторе инициализируем объект для поиска устройств:
Во-первых, создаём экземпляр QBluetoothDeviceDiscoveryAgent, которому в качестве аргумента передаём адрес текущего Bluetooth устройства. Затем присоединяем два сигнала объекта к нашему текущему: deviceDiscovered() для обработки нового найденного устройства и finished() для обработки завершения поиска.
Метод для начала поиска содержит следующие строчки:
Здесь мы сохраняем сообщение, которое необходимо передать и начинаем поиск устройств.
Для обработки найденных устройств используется слот deviceDiscovered(), к которому ранее мы уже подключили сигнал:
Как упоминалось ранее мы смотрим на список уникальных идентификаторов сервисов для поиска в нём нашего зарегистрированного. При первом найденном устройстве, которое предоставляет искомый сервис, мы завершаем поиск и вызываем метод для установления сопряжения между устройствами.
Сопряжение устройств является важным аспектом во взаимодействии устройств с помощью Bluetooth. Это подразумевает собой, что два устройства устанавливают между собой доверительные отношения и им доступен более широкий спектр возможностей взаимодействия (например, дистанционное управление). Конкретно в нашем примере сопряжение не требуется, но мы установим его, чтобы разобрать как это делается в общем случае. Установка сопряжения требует повышенных привилегий.
Для сопряжения устройств используется класс QBluetoothLocalDevice. Мы уже использовали его ранее в коде серверной части для получения адреса текущего устройства. Он же используется и для сопряжения устройств. Нас интересуют методы:
и сигналы:
Адрес удалённого устройства мы можем получить с помощью вызова метода address() на экземпляре QBluetoothDeviceInfo, в дальнейшем будем использовать его при установке сопряжения и подключения к сервису. Теперь попробуем установить сопряжение между двумя устройствами. Для начала добавим подключение к сигналам в конструктор класса клиента:
Экземпляр QBluetoothLocalDevice в данном случае является полем класса. Слот pairingFinished() содержит строчку, запускающую клиент startClient(address), а pairingError() – отладочный вывод.
Для установки сопряжения мы реализовали метод requestPairing() со следующим содержанием:
Если устройства уже сопряжены, то просто инициируем подключение к серверу, в противном случае запрашиваем сопряжение. В результате при успешном установлении сопряжения также инициируется подключение к серверу, а при ошибке уведомляем пользователя о проблеме.
Экземпляр класса QBluetoothDeviceInfo, соответствующий найденному устройству, содержит метод для получения адреса, которого достаточно для подключения к сервису. Для этого используется QBluetoothSocket, достаточно создать экземпляр данного класса с помощью конструктора, передав ему протокол RFCOMM и вызвать метод connectToService(), которому в качестве аргументов передаётся адрес из экземпляра QBluetoothDeviceInfo и порт, по которому необходимо установить соединение. Чтобы установить соединение с сервисом, необходимо указывать порт 1.
Теперь рассмотрим процесс установки соединения, передачи и приёма данных с помощью сокета. В клиенте используется тот же самый QBluetoothSocket что и на сервере, что позволяет нам использовать рассмотренные ранее сигналы для реализации обработчиков и методы для записи данных в сокет. Метод startClient() устанавливает соединение с устройством, предоставляющим сервис, с помощью сокета:
Создаём экземпляр сокета с протоколом RFCOMM и подключаем его сигналы к слотам нашего класса. Затем вызовом метода connectToService() подключаемся к другому устройству. Следует заметить, что если бы мы использовали класс QBluetoothServiceInfo, который позволяет получить информацию о найденных сервисах в виде экземпляров QBluetoothServiceInfo, то достаточно было бы вызвать метод connectToService() с одним аргументом, принимающим информацию о сервисе.
Метод socketConnected() вызывается при установке подключения по сокету, внутри него мы отправляем данные на сервер:
Здесь используется тот же самый класс сокета, что и на сервере, так что мы можем передавать любые данные в виде массива байтов.
Как мы помним, код сервера позволяет получить строку, развернуть её и вернуть нам, для обработки входящего сообщения мы соединили слот readSocket() с сигналом readyRead(). Выглядит этот слот следующим образом:
В итоге мы покрыли большую часть функционала, требующегося для реализации сервера и клиента для передачи данных любого рода с между ними. Также рассмотрели процедуру поиска устройств. Материала, упомянутого в статье, достаточно для реализации передачи любых данных между двумя устройствами. Код приложения-примера доступен на GitHub.
Технические вопросы можно также обсудить на канале русскоязычного сообщества Sailfish OS в Telegram или группе ВКонтакте.
Автор: Сергей Аверкиев
Bluetooth
Технология Bluetooth позволяет создавать беспроводное соединение, благодаря которому возможна передача абсолютно любых данных. Существует целый ряд задач для которых Bluetooth является общепринятым решением, например, передача файлов с одного устройства на другое, подключение к Bluetooth гарнитурам или дистанционное управление сканерами и принтерами.
Приложение-пример
Будем рассматривать использование технологии Bluetooth на примере реализации приложения для обмена строками. Назовём его bluetooth-messenger. Приложение будет работать в двух режимах: сервер и клиент. Сервер будет регистрировать Bluetooth сервис и реагировать на подключение клиента. Клиент же будет искать созданный сервис, подключаться к нему и передавать данные. Как следствие, необходимо наличие двух устройств, работающих под управлением Sailfish OS.
В итоге приложение будет работать следующим образом:
- Клиент ищет сервер с зарегистрированным сервисом.
- Найденному серверу передаёт строку.
- Сервер принимает строку, отображает её на экране.
- Принятая строка разворачивается и передаётся обратно клиенту.
- Клиент отображает развёрнутую строку на экране и отключается от сервера.
Реализация данного приложения позволит полностью осветить необходимые инструменты для установки связи между двумя устройствами и обмена данными между ними.
Предоставление повышенных привилегий приложению
Для взаимодействия с Bluetooth приложению иногда требуются повышенные привилегии (для поиска сервисов, изменения настроек видимости, для сопряжения устройств). При отсутствии повышенных привилегий часть функционала будет недоступна, поэтому рекомендуем выдавать их приложению, работающему с Bluetooth.
Для отладки необходимо запускать приложение с помощью devel-su с флагом -p. Это позволяет запускать приложение с повышенными привилегиями, а отладочный вывод будет доступен в консоли.
devel-su -p /usr/bin/bluetooth-messenger
Для того, чтобы запустить приложение с повышенными привилегиями по нажатию на иконку, необходимо сделать некоторые настройки в исходных файлах проекта. Во-первых, исполняемый файл приложения нужно запускать с помощью invoker. Invoker находит главную функцию приложения и запускает её с переданными ему аргументами. Это настраивается в .desktop файле проекта следующей строчкой:
Exec=invoker --type=silica-qt5 -s /usr/bin/bluetooth-messenger
Во-вторых, необходимо создать файл с названием, соответствующим названию исполняемого файла, в директории /usr/share/mapplauncherd/privileges.d/ и поместить туда строчку:
/usr/bin/bluetooth-messenger,
Запятая в конце строчки обязательно нужна. Таким образом при нажатии на иконку приложения, пользователь запустит его с повышенными привилегиями.
Управление состоянием Bluetooth
Для начала необходимо понять, каким образом возможно управлять состоянием Bluetooth. Для этого следует использовать систему D-Bus, взаимодействие с которой было описано в одной из предыдущих статей. С помощью данной системы мы имеем возможность включать и выключать питание Bluetooth и настраивать видимость для других устройств.
Для включения Bluetooth необходимо использовать сервис net.connman. На интерфейсе net.connman по пути /net/connman/technology/bluetooth есть метод SetProperty, с помощью которого возможно установить значение свойства Powered, которое отвечает за то включен Bluetooth или нет. Устанавливается свойство следующим образом:
QDBusInterface bluetoothInterface("net.connman", "/net/connman/technology/bluetooth",
"net.connman.Technology", QDBusConnection::systemBus(), this);
bluetoothInterface.call("SetProperty", "Powered", QVariant::fromValue(QDBusVariant(true)));
Создаём экземпляр QDBusInterface с использованием перечисленных ранее сервиса, пути и интерфейса. Затем на интерфейсе вызываем метод SetProperty с двумя аргументами: названием свойства и значением.
После включения Bluetooth будет полезно настроить видимость для других устройств. Для этого используем сервис org.bluez. Во-первых необходимо получить путь, соответствующий текущему устройству. Для этого по корневому пути на интерфейсе org.bluez.Manager вызываем метод DefaultAdapter, содержащий в выходных аргументах путь к текущему адаптеру, который впоследствии мы будем использовать для установки видимости.
QDBusInterface adapterListInterface("org.bluez", "/", "org.bluez.Manager",
QDBusConnection::systemBus(), this);
QVariant adapterPath = adapterListInterface.call("DefaultAdapter").arguments().at(0);
После получения пути для установки видимости необходимо использовать метод SetProperty на интерфейсе org.bluez.Adapter для установки следующих свойств:
- DiscoverableTimeout – время в секундах (unsigned int), в течении которого устройство будет возможно обнаружить после включения обнаружения. Если установлено значение 0, то обнаружение запускается без таймера.
- Discoverable – в зависимости от значения true или false включает или выключает обнаружение.
Неограниченное по времени обнаружение включаем следующими строчками:
QDBusInterface bluetoothAdapter("org.bluez", adapterPath.value<QDBusObjectPath>().path(),
"org.bluez.Adapter", QDBusConnection::systemBus(), this);
bluetoothAdapter.call("SetProperty", "DiscoverableTimeout", QVariant::fromValue(QDBusVariant(0U)));
bluetoothAdapter.call("SetProperty", "Discoverable", QVariant::fromValue(QDBusVariant(true)));
Следует заметить, что для настройки опций видимости необходимо, чтобы приложение было запущено с повышенными привилегиями.
Регистрация сервиса Bluetooth
Для начала нам необходимо создать сервер и зарегистрировать на нём сервис. Данный сервис будет принимать сообщения от клиентов и отвечать им. Для решения данной задачи мы создадим класс MessengerServer, заголовочный файл которого будет содержать следующее:
class MessengerServer : public QObject {
Q_OBJECT
public:
explicit MessengerServer(QObject *parent = 0);
~MessengerServer();
Q_INVOKABLE void startServer();
Q_INVOKABLE void stopServer();
signals:
void messageReceived(QString message);
private:
QBluetoothServer *bluetoothServer;
QBluetoothServiceInfo serviceInfo;
QBluetoothSocket *socket;
const QString SERVICE_UUID = "1f2d6c5b-6a86-4b30-8b4e-3990043d73f1";
private slots:
void clientConnected();
void clientDisconnected();
void readSocket();
};
Теперь более детально рассмотрим компоненты и содержание методов данного класса.
Устройство может оповестить другие устройства, осуществляющие поиск по Bluetooth путём регистрации сервиса. Для этого используется класс QBluetoothServer. С его помощью можно создать Bluetooth сервер и зарегистрировать на нём сервис, который будет сообщать устройствам, что он из себя представляет.
QBluetoothServer содержит набор методов для установки сервера на устройстве и регистрации сервиса. В частности представляют интерес:
- Конструктор QBluetoothServer(QBluetoothServiceInfo::Protocol serverType, QObject* parent) – служит для инициализации сервера, принимает в качестве аргументов протокол и родительский QObject. В нашем примере будем использовать протокол RFCOMM.
- Метод listen(const QBluetoothAddress& address, quint16 port) – начинает прослушку входящих подключений по переданным адресу и порту.
- Сигнал error(QBluetoothServer::Error error) – вызывается при возникновении ошибок сервера (Bluetooth выключен, сервис уже зарегистрирован и др.), где в качестве аргумента доступна сама ошибка.
- Сигнал newConnection() – вызывается при новом запросе на подключение.
Остальные методы служат для остановки сервера, получения текущего адреса или порта, проверки статуса и другие. Они не настолько интересны, и о них вы можете почитать в официальной документации.
После того как мы подняли сервер, необходимо зарегистрировать сервис. Сервис представляет собой описание какой-либо службы, выполняющей определённые обязанности. Описывается сервис с помощью объекта QBluetoothServiceInfo путём установки атрибутов выделенными методами. Для решения поставленной выше задачи используем метод startServer():
bluetoothServer = new QBluetoothServer(QBluetoothServiceInfo::RfcommProtocol, this);
connect(bluetoothServer, &QBluetoothServer::newConnection,
this, &MessengerServer::clientConnected);
QBluetoothAddress bluetoothAddress = QBluetoothLocalDevice().address();
bluetoothServer->listen(bluetoothAddress);
Первой строчкой мы создаём сервер, который в качестве протокола использует RFCOMM. Затем соединяем сигнал о новом подключении со слотом нашего класса. После этого включаем прослушивание на нашем адресе, для чего создаём экземпляр текущего устройства, из которого извлекаем его адрес и передаём методу listen(). Таким образом мы устанавливаем сервер.
Регистрация сервиса требует большего количества кода для указания всех требующихся для его работы параметров:
serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceName, "BT message sender");
serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceDescription,
"Example message sender");
serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceProvider, "fruct.org");
serviceInfo.setServiceUuid(QBluetoothUuid(SERVICE_UUID));
Здесь устанавливаем название сервиса, описание, поставщика сервиса (например название компании) и уникальный идентификатор (в данном приложении содержится в константе в виде строки и задаётся в формате xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, где x – это шестнадцатеричное число). Первые три атрибута позволяют получить базовое представление о найденном сервисе в то время как четвёртый может использоваться устройствами для поиска конкретного сервиса.
QBluetoothServiceInfo::Sequence classId;
classId << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::SerialPort));
serviceInfo.setAttribute(QBluetoothServiceInfo::BluetoothProfileDescriptorList, classId);
classId.prepend(QVariant::fromValue(QBluetoothUuid(SERVICE_UUID)));
serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceClassIds, classId);
Конструкция подобного рода использует последовательность (QBluetoothServiceInfo::Sequence) для установки прочих атрибутов. В данном случае мы устанавливаем уникальный идентификатор сервиса. Таким образом сервер даёт знать о том, какие сервисы он предоставляет.
QBluetoothServiceInfo::Sequence publicBrowse;
publicBrowse << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::PublicBrowseGroup));
serviceInfo.setAttribute(QBluetoothServiceInfo::BrowseGroupList, publicBrowse);
Данными строчками устанавливаем группу общедоступного поиска, что позволит устройствам свободно находить данный сервис. В противном случае сервис найден не будет.
QBluetoothServiceInfo::Sequence protocol;
QBluetoothServiceInfo::Sequence protocolDescriptorList;
protocol << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::Rfcomm))
<< QVariant::fromValue(quint8(bluetoothServer->serverPort()));
protocolDescriptorList.append(QVariant::fromValue(protocol));
serviceInfo.setAttribute(QBluetoothServiceInfo::ProtocolDescriptorList, protocolDescriptorList);
Здесь для доступа к сервису устанавливаем протокол RFCOMM, аналогично используемому сервером.
serviceInfo.registerService(bluetoothAddress);
Наконец, выполняем регистрацию созданного сервиса на адресе, полученном ранее и используемом сервером. С этого момента сервис будет виден при поиске по Bluetooth другими устройствами.
Работа с входящими соединениями
Теперь, когда сервис зарегистрирован и приложение готово принимать входящие соединения, необходимо их обрабатывать. Как упоминалось ранее, сервер приложения должен принимать строку от клиента, разворачивать её и отправлять обратно.
При подключении клиента к серверу мы создаём сокет, представленный в виде экземпляра QBluetoothSocket, который можно получить вызвав метод nextPendingConnection() на экземпляре класса QBluetoothServer. У сокета есть целый набор сигналов, позволяющих отследить его состояние, наиболее полезными из которых являются:
- connected() – вызывается при создании соединения по сокету.
- disconnected() – вызывается при разрыве соединения.
- error(QBluetoothSocket::SocketError error) – вызывается при возникновении ошибки, в качестве аргумента передаётся её тип.
- readyRead() – вызывается, когда в сокете доступны новые данные для чтения.
Используем их для обработки входящих соединений. Ранее мы прикрепили сигнал newConnection() к слоту clientConnected(), рассмотрим его реализацию.
void MessengerServer::clientConnected() {
//...
socket = bluetoothServer->nextPendingConnection();
connect(socket, &QBluetoothSocket::readyRead, this, &MessengerServer::readSocket);
connect(socket, &QBluetoothSocket::disconnected, this, &MessengerServer::clientDisconnected);
}
Объект QBluetoothSocket является наследником QIODevice, как следствие ему доступны методы для чтения линии, символа, выбранного количества символов и т.п. Методы для чтения (как и методы для записи) используют QByteArray, что позволяет передавать не только строки, но и любые другие данные в виде набора байтов. Таким образом возможна передача любых типов данных вне зависимости от содержимого.
В нашем примере для обработки входящих сообщений мы соединили сигнал readyRead() с методом readSocket(), код которого выглядит следующим образом:
void MessengerServer::readSocket() {
//...
const QString message = QString::fromUtf8(socket->readLine().trimmed());
emit messageReceived(message);
QString reversedMessage;
for (int i = message.size() - 1; i >= 0; i--) {
reversedMessage.append(message.at(i));
}
socket->write(reversedMessage.toUtf8());
}
Для чтения данных в виде массива байтов мы используем метод readLine(), после чего преобразуем считанную линию в строку, разворачиваем её и отправляем обратно с помощью метода write(), преобразовав обратно в массив байтов. Таким образом реализованный нами сервер способен получать строку от любого другого устройства по Bluetooth и возвращать её обратно в развёрнутом виде.
Поиск сервиса
Теперь, когда сервер реализован, запущен и ждёт входящих соединений, необходимо к нему подключиться. Каким образом возможно найти устройство, которое предоставляет необходимый сервис? Во-первых, необходимо произвести поиск сервисов, доступных на видимых Bluetooth устройствах и только затем к нему подключаться.
Заголовочный файл клиента имеет следующее содержание:
class MessengerClient : public QObject {
Q_OBJECT
public:
explicit MessengerClient(QObject *parent = 0);
~MessengerClient();
Q_INVOKABLE void startDiscovery(const QString &messageToSend);
Q_INVOKABLE void stopDiscovery();
private:
const QString SERVICE_UUID = "1f2d6c5b-6a86-4b30-8b4e-3990043d73f1";
QString message;
QBluetoothSocket *socket = NULL;
QBluetoothDeviceDiscoveryAgent* discoveryAgent;
QBluetoothDeviceInfo device;
QBluetoothLocalDevice localDevice;
void requestPairing(const QBluetoothAddress &address);
void startClient(const QBluetoothAddress &address);
void stopClient();
signals:
void messageReceived(QString message);
void clientStatusChanged(QString text);
private slots:
void deviceDiscovered(const QBluetoothDeviceInfo &deviceInfo);
void pairingFinished(const QBluetoothAddress &address, QBluetoothLocalDevice::Pairing pairing);
void pairingError(QBluetoothLocalDevice::Error error);
void socketConnected();
void deviceSearchFinished();
void readSocket();
};
Рассмотрим компоненты, которые необходимы для реализации поиска сервиса и отправки сообщений.
Для поиска сервисов библиотека Qt предоставляет класс QBluetoothServiceDiscoveryAgent. Он позволяет автоматически проверить все устройства на наличие определённого сервиса, который мы ищем по UUID. В дальнейшем, при нахождении сервиса объект данного класса инициирует соответствующий сигнал, с помощью которого мы можем обработать результат поиска. Следует заметить, что использование данного класса требует, чтобы приложение было запущено с повышенными привилегиями. Класс содержит следующие интересующие нас методы:
- setUuidFilter(const QBluetoothUuid &uuid) – устанавливает UUID, сервис с которым требуется найти. Также есть аналогичный метод для установки нескольких UUID.
- setRemoteAddress(const QBluetoothAddress &address) – устанавливает адрес устройства, на котором необходимо найти сервис. Может использоваться, если точно известен адрес устройства, которое необходимо найти.
- start() – запускает поиск сервисов.
- stop() – останавливает поиск сервисов.
- discoveredServices() – возвращает список найденных сервисов.
- clear() – очищает список найденных сервисов.
Для обработки результата полезны сигналы:
- serviceDiscovered(const QBluetoothServiceInfo &info) – вызывается при обнаружении сервиса, информация о нём передаётся с аргументом.
- finished() – вызывается при завершении поиска.
- error(QBluetoothServiceDiscoveryAgent::Error error) – вызывается при возникновении ошибок.
Для поиска нашего конкретного сервиса необходимо установить методом setUuidFilter() фильтр по UUID, который мы указывали при регистрации сервиса и методом start() начать поиск. После этого при обнаружении нашего сервиса будет инициирован сигнал serviceDiscovered(). Экземпляр QBluetoothServiceInfo содержит информацию о найденном сервисе (имя, UUID, информация об устройстве, на котором он зарегистрирован и т.п.). Экземпляр данного класса мы будем использовать для подключения к сервису, о чём будет упомянуто вдальнейшем.
Конкретно в нашем примере будем рассматривать другой класс, не требующий повышенных привилегий – QBluetoothDeviceDiscoveryAgent. С его помощью возможен поиск устройств, а не сервисов, и он не требует повышенных привилегий. Для каждого найденного устройства будем просматривать сервисы, зарегистрированные на устройстве, и если в списке есть наш сервис, то считаем сервис найденным и в дальнейшем будем подключаться к нему.
QBluetoothDeviceDiscoveryAgent состоит из небольшого числа методов для поиска устройств. Наиболее полезными являются следующие:
- start() – начинает поиск устройств.
- stop() – останавливает поиск устройств.
- discoveredDevices() – возвращает список всех найденных устройств.
- error() – возвращает тип последней возникшей при поиске ошибки. Также есть сигнал, который будет инициирован сразу после возникновения ошибки с типом ошибки в качестве аргумента.
- errorText() – возвращает текст последней возникшей ошибки.
Также в случае нахождения устройства будет немедленно инициирован сигнал deviceDiscovered(const QBluetoothDeviceInfo &info), который может служить для обработки результата.
Информация о найденных устройствах представлена в виде объекта QDeviceInfo. Из данного объекта можно извлечь данные с помощью специальных методов. Наиболее интересными являются следующие:
- address() – mac-адрес найденного устройства. Используется при поиске любых устройств кроме macOS и iOS.
- deviceUuid() – уникальный идентификатор найденного устройства. Используется только при поиске устройств на macOS и iOS.
- name() – имя найденного устройства.
- serviceUuids() – список уникальных идентификаторов зарегистрированных сервисов.
Теперь, когда мы знаем как искать сервисы на Bluetooth устройствах попробуем найти наш собственный сервис. В конструкторе инициализируем объект для поиска устройств:
MessengerClient::MessengerClient(QObject *parent) : QObject(parent) {
//...
discoveryAgent = new QBluetoothDeviceDiscoveryAgent(localDevice.address());
connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered,
this, &MessengerClient::deviceDiscovered);
connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished,
this, &MessengerClient::deviceSearchFinished);
//...
}
Во-первых, создаём экземпляр QBluetoothDeviceDiscoveryAgent, которому в качестве аргумента передаём адрес текущего Bluetooth устройства. Затем присоединяем два сигнала объекта к нашему текущему: deviceDiscovered() для обработки нового найденного устройства и finished() для обработки завершения поиска.
Метод для начала поиска содержит следующие строчки:
void MessengerClient::startDiscovery(const QString &messageToSend) {
//...
this->message = messageToSend;
discoveryAgent->start();
//...
}
Здесь мы сохраняем сообщение, которое необходимо передать и начинаем поиск устройств.
Для обработки найденных устройств используется слот deviceDiscovered(), к которому ранее мы уже подключили сигнал:
void MessengerClient::deviceDiscovered(const QBluetoothDeviceInfo &deviceInfo) {
//...
if (deviceInfo.serviceUuids().contains(QBluetoothUuid(SERVICE_UUID))) {
emit clientStatusChanged(QStringLiteral("Device found"));
discoveryAgent->stop();
requestPairing(deviceInfo.address());
}
}
Как упоминалось ранее мы смотрим на список уникальных идентификаторов сервисов для поиска в нём нашего зарегистрированного. При первом найденном устройстве, которое предоставляет искомый сервис, мы завершаем поиск и вызываем метод для установления сопряжения между устройствами.
Сопряжение устройств
Сопряжение устройств является важным аспектом во взаимодействии устройств с помощью Bluetooth. Это подразумевает собой, что два устройства устанавливают между собой доверительные отношения и им доступен более широкий спектр возможностей взаимодействия (например, дистанционное управление). Конкретно в нашем примере сопряжение не требуется, но мы установим его, чтобы разобрать как это делается в общем случае. Установка сопряжения требует повышенных привилегий.
Для сопряжения устройств используется класс QBluetoothLocalDevice. Мы уже использовали его ранее в коде серверной части для получения адреса текущего устройства. Он же используется и для сопряжения устройств. Нас интересуют методы:
- pairingStatus(const QBluetoothAddress &address) – позволяет получить статус сопряжения между текущим устройством и устройством по адресу. Возвращает одно из значений:
- requestPairing(const QBluetoothAddress &address, Pairing pairing) – запрашивает изменения статуса сопряжения с устройством (вторым аргументом передаётся Paired для установки сопряжения или Unpaired для разрыва).
и сигналы:
- pairingFinished(const QBluetoothAddress &address, QBluetoothLocalDevice::Pairing pairing) – возвращается при успешном изменении статуса сопряжения.
- error(QBluetoothLocalDevice::Error error) – возвращается при ошибке изменения статуса сопряжения (в том числе отмена предложения о сопряжении на одном из устройств).
Адрес удалённого устройства мы можем получить с помощью вызова метода address() на экземпляре QBluetoothDeviceInfo, в дальнейшем будем использовать его при установке сопряжения и подключения к сервису. Теперь попробуем установить сопряжение между двумя устройствами. Для начала добавим подключение к сигналам в конструктор класса клиента:
connect(&localDevice, &QBluetoothLocalDevice::pairingFinished,
this, &MessengerClient::pairingFinished);
connect(&localDevice, &QBluetoothLocalDevice::error, this, &MessengerClient::pairingError);
Экземпляр QBluetoothLocalDevice в данном случае является полем класса. Слот pairingFinished() содержит строчку, запускающую клиент startClient(address), а pairingError() – отладочный вывод.
Для установки сопряжения мы реализовали метод requestPairing() со следующим содержанием:
void MessengerClient::requestPairing(const QBluetoothAddress &address) {
//...
if (localDevice.pairingStatus(address) == QBluetoothLocalDevice::Paired) {
startClient(address);
} else {
localDevice.requestPairing(address, QBluetoothLocalDevice::Paired);
}
}
Если устройства уже сопряжены, то просто инициируем подключение к серверу, в противном случае запрашиваем сопряжение. В результате при успешном установлении сопряжения также инициируется подключение к серверу, а при ошибке уведомляем пользователя о проблеме.
Подключение к серверу
Экземпляр класса QBluetoothDeviceInfo, соответствующий найденному устройству, содержит метод для получения адреса, которого достаточно для подключения к сервису. Для этого используется QBluetoothSocket, достаточно создать экземпляр данного класса с помощью конструктора, передав ему протокол RFCOMM и вызвать метод connectToService(), которому в качестве аргументов передаётся адрес из экземпляра QBluetoothDeviceInfo и порт, по которому необходимо установить соединение. Чтобы установить соединение с сервисом, необходимо указывать порт 1.
Теперь рассмотрим процесс установки соединения, передачи и приёма данных с помощью сокета. В клиенте используется тот же самый QBluetoothSocket что и на сервере, что позволяет нам использовать рассмотренные ранее сигналы для реализации обработчиков и методы для записи данных в сокет. Метод startClient() устанавливает соединение с устройством, предоставляющим сервис, с помощью сокета:
void MessengerClient::startClient(const QBluetoothDeviceInfo &deviceInfo) {
//...
socket = new QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol, this);
connect(socket, &QBluetoothSocket::connected, this, &MessengerClient::socketConnected);
connect(socket, &QBluetoothSocket::readyRead, this, &MessengerClient::readSocket);
socket->connectToService(deviceInfo.address(), 1);
}
Создаём экземпляр сокета с протоколом RFCOMM и подключаем его сигналы к слотам нашего класса. Затем вызовом метода connectToService() подключаемся к другому устройству. Следует заметить, что если бы мы использовали класс QBluetoothServiceInfo, который позволяет получить информацию о найденных сервисах в виде экземпляров QBluetoothServiceInfo, то достаточно было бы вызвать метод connectToService() с одним аргументом, принимающим информацию о сервисе.
Метод socketConnected() вызывается при установке подключения по сокету, внутри него мы отправляем данные на сервер:
void MessengerClient::socketConnected() {
//...
socket->write(message.toUtf8());
}
Здесь используется тот же самый класс сокета, что и на сервере, так что мы можем передавать любые данные в виде массива байтов.
Как мы помним, код сервера позволяет получить строку, развернуть её и вернуть нам, для обработки входящего сообщения мы соединили слот readSocket() с сигналом readyRead(). Выглядит этот слот следующим образом:
void MessengerClient::readSocket() {
//...
QString receivedMessage = QString::fromUtf8(socket->readLine().trimmed());
emit messageReceived(receivedMessage);
}
Результат
В итоге мы покрыли большую часть функционала, требующегося для реализации сервера и клиента для передачи данных любого рода с между ними. Также рассмотрели процедуру поиска устройств. Материала, упомянутого в статье, достаточно для реализации передачи любых данных между двумя устройствами. Код приложения-примера доступен на GitHub.
Технические вопросы можно также обсудить на канале русскоязычного сообщества Sailfish OS в Telegram или группе ВКонтакте.
Автор: Сергей Аверкиев