Sailfish OSの開発:Bluetoothでの作業

こんにちは この記事は、モバイルプラットフォームSailfish OSの開発に関する一連の記事の続きです。 今回は、Bluetoothを使用して2つのデバイス間の接続を確立し、データを転送することについて説明します。



Bluetooth



Bluetoothテクノロジを使用すると、ワイヤレス接続を作成できます。これにより、あらゆるデータを完全に転送できます。 Bluetoothが一般的なソリューションである多くのタスクがあります。たとえば、あるデバイスから別のデバイスにファイルを転送したり、Bluetoothヘッドセットに接続したり、スキャナーやプリンターをリモート制御したりします。



応用例



文字列を交換するためのアプリケーションの実装例として、Bluetoothテクノロジーの使用を検討します。 bluetooth-messengerと呼びましょう。 アプリケーションは、サーバーとクライアントの2つのモードで動作します。 サーバーはBluetoothサービスを登録し、クライアントの接続に応答します。 クライアントは作成されたサービスを検索し、それに接続してデータを転送します。 そのため、Sailfish OSを実行する2つのデバイスが必要です。











その結果、アプリケーションは次のように機能します。



  1. クライアントは、登録されたサービスを持つサーバーを探しています。
  2. 見つかったサーバーには文字列が渡されます。
  3. サーバーは文字列を受け入れ、画面に表示します。
  4. 受信した文字列は展開され、クライアントに返されます。
  5. クライアントは、画面に展開された行を表示し、サーバーから切断します。












このアプリケーションの実装は、2つのデバイス間の通信を確立し、それらの間でデータを交換するために必要なツールを完全に明らかにします。



アプリケーションに昇格した特権を付与する



Bluetoothとやり取りするために、アプリケーションは(サービスの検索、表示設定の変更、デバイスのペアリングのために)昇格された特権を必要とする場合があります。 昇格された特権がないと、一部の機能が利用できなくなるため、Bluetoothで動作するアプリケーションにそれらを提供することをお勧めします。



デバッグするには、 -pフラグを指定したdevel-suを使用してアプリケーションを実行する必要があります。 これにより、昇格した特権でアプリケーションを実行でき、コンソールでデバッグ出力が利用可能になります。



devel-su -p /usr/bin/bluetooth-messenger
      
      





アイコンをクリックして昇格した特権でアプリケーションを実行するには、プロジェクトのソースファイルでいくつかの設定を行う必要があります。 まず、 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システムを使用します。D-Busシステムとの相互作用については、以前の記事のいずれかで説明しました。 このシステムを使用すると、Bluetoothの電源をオンまたはオフにして、他のデバイスの可視性を調整できます。



Bluetoothを有効にするには、 net.connmanサービスを使用する必要があります。 パス/ net / connman / technology / bluetoothに沿ったnet.connmanインターフェースには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のインスタンスを作成します。 次に、インターフェイスで、プロパティの名前と値の2つの引数を使用して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);
      
      





可視性を設定するためのパスを取得したら、 org.bluez.AdapterインターフェースのSetPropertyメソッドを使用して、次のプロパティを設定する必要があります





無制限の時間検出には、次の行が含まれます。



 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には、サーバーをデバイスにインストールし、サービスを登録するための一連のメソッドが含まれています。 特に、それらは興味深いものです:





他の方法は、サーバーの停止、現在のアドレスまたはポートの取得、ステータスの確認などに使用されます。 それらはそれほど面白くありません。公式ドキュメントでそれらについて読むことができます。



サーバーを上げた後、サービスを登録する必要があります。 サービスは、特定の職務を実行するサービスの説明です。 サービスは、選択されたメソッドを使用して属性を設定することにより、 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は16進数です)。 最初の3つの属性を使用すると、見つかったサービスの基本的な概念を取得できますが、4番目の属性はデバイスが特定のサービスを検索するために使用できます。



 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のインスタンスとして提示されるソケットを作成します。これは、 QBluetoothServerクラスのインスタンスでnextPendingConnection()メソッドを呼び出すことで取得できます。 ソケットには、そのステータスを追跡できる一連のシグナルがありますが、最も有用なものは次のとおりです。





これらを使用して、着信接続を処理します。 前に、シグナル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で探している特定のサービスのすべてのデバイスを自動的にチェックできます。 将来、サービスが見つかると、このクラスのオブジェクトは適切なシグナルを開始し、それを使用して検索結果を処理できます。 このクラスを使用するには、昇格した特権でアプリケーションを実行する必要があることに注意してください。 このクラスには、興味のある次のメソッドが含まれています。





次のシグナルは、結果の処理に役立ちます。





特定のサービスを検索するには、サービスの登録時に指定したUUIDフィルターでsetUuidFilter()メソッドを設定し、検索を開始する()必要があります。 その後、サービスが検出されると、シグナルserviceDiscovered()がトリガーされます。 QBluetoothServiceInfoインスタンスには、見つかったサービスに関する情報(名前、UUID、登録されているデバイスに関する情報など)が含まれています。 このクラスのインスタンスを使用してサービスに接続しますが、これについては後述します。



具体的には、この例では、昇格された特権を必要としない別のクラスQBluetoothDeviceDiscoveryAgentを検討します。 その助けを借りて、サービスではなくデバイスを検索することができ、昇格した権限は必要ありません。 見つかったデバイスごとに、デバイスに登録されているサービスを表示します。サービスがリストにある場合は、見つかったサービスを検討し、接続を継続します。



QBluetoothDeviceDiscoveryAgentは、デバイスを見つけるための少数のメソッドで構成されています。 最も有用なものは次のとおりです。





また、デバイスが見つかった場合、deviceDiscoveredシグナル(const QBluetoothDeviceInfo&info)がすぐにトリガーされ、結果の処理に役立ちます。



見つかったデバイスに関する情報は、 QDeviceInfoオブジェクトとして表示されます。 特別な方法を使用して、このオブジェクトからデータを抽出できます。 最も興味深いのは次のとおりです。





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); //... }
      
      





最初に、現在のBluetoothデバイスのアドレスを引数として渡すQBluetoothDeviceDiscoveryAgentのインスタンスを作成します。 次に、オブジェクトの2つの信号を現在の信号に添付します。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を使用したデバイスの相互作用における重要な側面です。 これは、2つのデバイスが相互に信頼関係を確立し、より広範な対話オプション(リモートコントロールなど)を使用できることを意味します。 具体的には、この例ではペアリングは必要ありませんが、一般的なケースでこれがどのように行われるかを理解するためにペアリングをインストールします。 ペアリングには昇格した権限が必要です。



デバイスをペアリングするには、 QBluetoothLocalDeviceクラスが使用されます。 サーバー側のコードで以前に使用して、現在のデバイスのアドレスを取得しました。 デバイスのペアリングにも使用されます。 メソッドに興味があります:





および信号:





QBluetoothDeviceInfoインスタンスでaddress()メソッドを呼び出すことにより、リモートデバイスのアドレスを取得できます。将来的には、サービスへのペアリングおよび接続時にそれを使用します。 次に、2つのデバイスのペアリングを確立してみます。 最初に、クライアントクラスコンストラクターに信号接続を追加します。



 connect(&localDevice, &QBluetoothLocalDevice::pairingFinished, this, &MessengerClient::pairingFinished); connect(&localDevice, &QBluetoothLocalDevice::error, this, &MessengerClient::pairingError);
      
      





この場合のQBluetoothLocalDeviceインスタンスはクラスフィールドです。 pairingFinished()スロットには、 startClient(アドレス)クライアントを開始する行が含まれ、 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クラスを使用した場合は、サービスに関する情報を取得する引数を1つ指定しconnectToService()メソッドを呼び出すだけで十分です。socketConnected()メソッドは、ソケット接続を確立するときに呼び出され、その内部でサーバーにデータを送信します。







 void MessengerClient::socketConnected() { //... socket->write(message.toUtf8()); }
      
      







サーバーと同じソケットクラスを使用するため、任意のデータをバイトの配列として転送できます。



私たちが覚えているように、サーバコードを使用すると、文字列を取得して展開し、受信メッセージを処理するために、私たちに復帰することができ、我々はスロットを組み合わせているreadSocket()シグナルとreadyReadを() このスロットは次のようになります。



 void MessengerClient::readSocket() { //... QString receivedMessage = QString::fromUtf8(socket->readLine().trimmed()); emit messageReceived(receivedMessage); }
      
      





結果



その結果、サーバーとクライアントの間であらゆる種類のデータを転送するためにサーバーとクライアントを実装するために必要な機能のほとんどをカバーしました。また、デバイスを見つける手順を調べました。この記事に記載されている資料は、2つのデバイス間でデータの転送を実装するのに十分です。サンプルアプリケーションコードはGitHubで入手できます



技術的な問題は、ロシア語を話すコミュニティのSailfish OSのTelegramまたはVKontakteグループチャネルでも議論できます。



著者:セルゲイ・アヴェルキエフ



All Articles