ربط أجهزة تتضمّن بلوتوث

لإنشاء اتصال بين جهازين، يجب تنفيذ الآليات من جهة الخادم والآلية من جهة العميل لأنه يجب أن يفتح أحد الأجهزة مقبس الخادم، والآخر يجب أن يبدأ الاتصال باستخدام عنوان MAC لجهاز الخادم. يحصل كل من جهاز الخادم وجهاز العميل على BluetoothSocket المطلوبة بطرق مختلفة. يتلقى الخادم معلومات المقبس عند قبول الاتصال الوارد. يوفر العميل معلومات المقبس عندما يفتح قناة RFCOMM على الخادم.

يعتبر الخادم والعميل متصلَين ببعضهما البعض عندما يكون لكل منهما مفتاح BluetoothSocket متصل على قناة RFCOMM نفسها. عند هذه المرحلة، يمكن لكل جهاز الحصول على عمليات بث المدخلات والمخرجات، ويمكن بدء نقل البيانات، وهو ما تمت مناقشته في القسم حول نقل بيانات البلوتوث. يصف هذا القسم كيفية بدء الاتصال بين جهازين.

تأكد من امتلاكك أذونات بلوتوث المناسبة وإعداد تطبيقك لاستخدام البلوتوث قبل محاولة العثور على أجهزة بلوتوث.

تقنيات الربط

تتمثل إحدى أساليب التنفيذ في إعداد كل جهاز تلقائيًا كخادم بحيث يكون لكل جهاز مقبس خادم مفتوح ويستجيب للاتصالات. في هذه الحالة، يمكن لأي من الجهازين بدء اتصال مع الآخر وأن يصبح العميل. وبدلاً من ذلك، يمكن لأحد الأجهزة استضافة الاتصال بشكل صريح وفتح مقبس خادم عند الطلب، ويبدأ الجهاز الآخر الاتصال.


الشكل 1. مربع حوار إقران البلوتوث

الاتصال كخادم

عندما تريد توصيل جهازين، يجب أن يعمل أحدهما كخادم من خلال الضغط مع الاستمرار على BluetoothServerSocket. الغرض من مقبس الخادم هو الاستماع إلى طلبات الاتصال الواردة وتوفير BluetoothSocket متصل بعد قبول الطلب. عند الحصول على BluetoothSocket من BluetoothServerSocket، يمكن، ويجب تجاهل BluetoothServerSocket، ما لم تكن تريد أن يقبل الجهاز المزيد من الاتصالات.

لإعداد مقبس خادم وقبول الاتصال، أكمل الخطوات التالية:

  1. احصل على BluetoothServerSocket عبر الاتصال على listenUsingRfcommWithServiceRecord(String, UUID).

    السلسلة هي اسم معرَّف لخدمتك، يكتبه النظام تلقائيًا إلى إدخال قاعدة بيانات جديد لبروتوكول اكتشاف الخدمة (SDP) على الجهاز. ويكون هذا الاسم عشوائيًا ويمكن أن يكون اسم تطبيقك بكل بساطة. يتم أيضًا تضمين المعرّف الفريد العالمي (UUID) في إدخال بروتوكول وصف الجلسة (SDP)، ويشكّل الأساس لاتفاقية الربط مع جهاز العميل. بمعنى أنه عندما يحاول العميل الاتصال بهذا الجهاز، فإنه يحمل معرّف فريد عالمي (UUID) يحدّد الخدمة التي يريد الاتصال بها بشكل فريد. يجب أن تتطابق أرقام التعريف الفريدة العالمية (UUID) هذه حتى يتم قبول الاتصال.

    المعرّف الفريد العالمي (UUID) هو تنسيق موحّد 128 بت لمعرّف سلسلة يُستخدَم لتعريف المعلومات بشكل فريد. يتم استخدام UUID لتحديد المعلومات التي يجب أن تكون فريدة داخل نظام أو شبكة لأن احتمالية تكرار UUID تساوي صفرًا فعليًا. يتم إنشاؤها بشكل مستقل، دون استخدام سلطة مركزية. وفي هذه الحالة، يتم استخدامه لتحديد خدمة البلوتوث في تطبيقك بشكل فريد. للحصول على معرّف فريد عالمي (UUID) لاستخدامه مع تطبيقك، يمكنك استخدام أحد منشئي UUID العشوائيين العديدين على الويب، ثم إعداد معرّف UUID باستخدام fromString(String).

  2. يمكنك بدء الاستماع إلى طلبات الاتصال عن طريق الاتصال على الرقم accept().

    هذه مكالمة حظر. يتم إرجاعه عند قبول الاتصال أو حدوث استثناء. لا يتم قبول الاتصال إلا عندما يرسل جهاز بعيد طلب اتصال يحتوي على معرّف فريد عالمي (UUID) يتطابق مع ذلك المسجّل باستخدام مقبس خادم الاستماع هذا. عند نجاح هذا الإجراء، تعرض دالة accept() خطأ BluetoothSocket متّصل.

  3. إذا لم تكن تريد قبول عمليات ربط إضافية، يمكنك الاتصال على الرقم close().

    يؤدي استدعاء الطريقة هذه إلى إطلاق مقبس الخادم وجميع موارده، ولكنها لا تغلق BluetoothSocket المتصلة التي تم عرضها من خلال accept(). على عكس بروتوكول TCP/IP، لا يتيح بروتوكول RFCOMM سوى عميل واحد متصل لكل قناة في المرة الواحدة، لذلك يكون من المنطقي في معظم الحالات طلب close() على BluetoothServerSocket مباشرةً بعد قبول المقبس المتصل.

بما أنّ استدعاء accept() عبارة عن استدعاء حظر، فلا تنفّذه في سلسلة التعليمات الرئيسية لواجهة مستخدم النشاط. وتنفيذه في سلسلة محادثات أخرى يضمن أن يظل بإمكان تطبيقك الاستجابة لتفاعلات المستخدمين الأخرى. من المنطقي عادةً أن تنجز كل الأعمال التي تتضمّن BluetoothServerSocket أو BluetoothSocket في سلسلة محادثات جديدة يديرها تطبيقك. لإلغاء مكالمة محظورة مثل accept()، يمكنك الاتصال بـ close() على BluetoothServerSocket أو BluetoothSocket من سلسلة محادثات أخرى. يُرجى العلم أنّ جميع الطرق على BluetoothServerSocket أو BluetoothSocket خالية من سلسلة التعليمات.

مثال

فيما يلي سلسلة ترابط مبسطة لمكون الخادم الذي يقبل الاتصالات الواردة:

Kotlin

private inner class AcceptThread : Thread() {

   private val mmServerSocket: BluetoothServerSocket? by lazy(LazyThreadSafetyMode.NONE) {
       bluetoothAdapter?.listenUsingInsecureRfcommWithServiceRecord(NAME, MY_UUID)
   }

   override fun run() {
       // Keep listening until exception occurs or a socket is returned.
       var shouldLoop = true
       while (shouldLoop) {
           val socket: BluetoothSocket? = try {
               mmServerSocket?.accept()
           } catch (e: IOException) {
               Log.e(TAG, "Socket's accept() method failed", e)
               shouldLoop = false
               null
           }
           socket?.also {
               manageMyConnectedSocket(it)
               mmServerSocket?.close()
               shouldLoop = false
           }
       }
   }

   // Closes the connect socket and causes the thread to finish.
   fun cancel() {
       try {
           mmServerSocket?.close()
       } catch (e: IOException) {
           Log.e(TAG, "Could not close the connect socket", e)
       }
   }
}

Java

private class AcceptThread extends Thread {
   private final BluetoothServerSocket mmServerSocket;

   public AcceptThread() {
       // Use a temporary object that is later assigned to mmServerSocket
       // because mmServerSocket is final.
       BluetoothServerSocket tmp = null;
       try {
           // MY_UUID is the app's UUID string, also used by the client code.
           tmp = bluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
       } catch (IOException e) {
           Log.e(TAG, "Socket's listen() method failed", e);
       }
       mmServerSocket = tmp;
   }

   public void run() {
       BluetoothSocket socket = null;
       // Keep listening until exception occurs or a socket is returned.
       while (true) {
           try {
               socket = mmServerSocket.accept();
           } catch (IOException e) {
               Log.e(TAG, "Socket's accept() method failed", e);
               break;
           }

           if (socket != null) {
               // A connection was accepted. Perform work associated with
               // the connection in a separate thread.
               manageMyConnectedSocket(socket);
               mmServerSocket.close();
               break;
           }
       }
   }

   // Closes the connect socket and causes the thread to finish.
   public void cancel() {
       try {
           mmServerSocket.close();
       } catch (IOException e) {
           Log.e(TAG, "Could not close the connect socket", e);
       }
   }
}

في هذا المثال، نحتاج إلى اتصال وارد واحد فقط، فبمجرد قبول الربط والحصول على BluetoothSocket، يمرّر التطبيق BluetoothSocket الذي تم الحصول عليه إلى سلسلة محادثات منفصلة، ثم يغلق BluetoothServerSocket ويخرج عن الحلقة.

ويُرجى العِلم أنّه عندما يعيد accept() الخطأ BluetoothSocket، يكون المقبس متصلاً مسبقًا. لذلك، يجب عدم طلب الرمز connect() كما تفعل من جهة العميل.

تم تصميم طريقة manageMyConnectedSocket() الخاصة بالتطبيق لبدء سلسلة التعليمات الخاصة بنقل البيانات، وقد تمت مناقشتها في موضوع نقل بيانات البلوتوث.

يجب عادةً إغلاق "BluetoothServerSocket" فور الانتهاء من الاستماع إلى الاتصالات الواردة. في هذا المثال، يتم استدعاء close() فور الحصول على BluetoothSocket. يمكنك أيضًا توفير طريقة عامة في سلسلة التعليمات تؤدي إلى إغلاق BluetoothSocket الخاص في حال احتجت إلى إيقاف الاستماع على مقبس الخادم هذا.

الربط كعميل

لبدء اتصال بجهاز بعيد يقبل الاتصالات على مقبس خادم مفتوح، يجب أولاً الحصول على كائن BluetoothDevice يمثل الجهاز البعيد. للتعرّف على طريقة إنشاء BluetoothDevice، يمكنك الاطّلاع على البحث عن أجهزة تتضمّن بلوتوث. عليك بعد ذلك استخدام BluetoothDevice للحصول على BluetoothSocket وبدء عملية الربط.

تتمثل الإجراءات الأساسية في ما يلي:

  1. باستخدام BluetoothDevice، احصل على BluetoothSocket من خلال الاتصال على createRfcommSocketToServiceRecord(UUID).

    تعمل هذه الطريقة على إعداد كائن BluetoothSocket يسمح للعميل بالربط بـ BluetoothDevice. يجب أن يتطابق معرّف المعرّف الفريد العالمي (UUID) الذي تم تمريره هنا مع المعرّف الفريد العالمي (UUID) الذي يستخدمه جهاز الخادم عندما يطلب منه listenUsingRfcommWithServiceRecord(String, UUID) فتح BluetoothServerSocket الخاص به. لاستخدام معرّف فريد عالمي (UUID)، عليك ترميز سلسلة UUID برمز ثابت في تطبيقك، ثم الإشارة إليها من كل من الخادم ورمز العميل.

  2. يمكنك بدء الاتصال من خلال الاتصال بـ connect(). لاحظ أن هذه الطريقة هي استدعاء حظر.

    بعد استدعاء البرنامج لهذه الطريقة، يُجري النظام بحث SDP للعثور على الجهاز البعيد الذي يحتوي على معرّف فريد عالمي (UUID) مطابق. إذا كان البحث ناجحًا ووافق الجهاز البعيد على الاتصال، سيشارك قناة RFCOMM لاستخدامها أثناء الاتصال، ويتم عرض الطريقة connect(). إذا تعذَّر الاتصال أو إذا انتهت مهلة طريقة connect() (بعد 12 ثانية تقريبًا)، تعرض الطريقة IOException.

بما أنّ connect() هو استدعاء حظر، يجب دائمًا تنفيذ إجراء الربط هذا في سلسلة محادثات منفصلة عن سلسلة تعليمات النشاط الرئيسية (واجهة المستخدم).

مثال

في ما يلي مثال أساسي لسلسلة تعليمات من عميل تؤدّي إلى بدء اتصال بلوتوث:

Kotlin

private inner class ConnectThread(device: BluetoothDevice) : Thread() {

   private val mmSocket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) {
       device.createRfcommSocketToServiceRecord(MY_UUID)
   }

   public override fun run() {
       // Cancel discovery because it otherwise slows down the connection.
       bluetoothAdapter?.cancelDiscovery()

       mmSocket?.let { socket ->
           // Connect to the remote device through the socket. This call blocks
           // until it succeeds or throws an exception.
           socket.connect()

           // The connection attempt succeeded. Perform work associated with
           // the connection in a separate thread.
           manageMyConnectedSocket(socket)
       }
   }

   // Closes the client socket and causes the thread to finish.
   fun cancel() {
       try {
           mmSocket?.close()
       } catch (e: IOException) {
           Log.e(TAG, "Could not close the client socket", e)
       }
   }
}

Java

private class ConnectThread extends Thread {
   private final BluetoothSocket mmSocket;
   private final BluetoothDevice mmDevice;

   public ConnectThread(BluetoothDevice device) {
       // Use a temporary object that is later assigned to mmSocket
       // because mmSocket is final.
       BluetoothSocket tmp = null;
       mmDevice = device;

       try {
           // Get a BluetoothSocket to connect with the given BluetoothDevice.
           // MY_UUID is the app's UUID string, also used in the server code.
           tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
       } catch (IOException e) {
           Log.e(TAG, "Socket's create() method failed", e);
       }
       mmSocket = tmp;
   }

   public void run() {
       // Cancel discovery because it otherwise slows down the connection.
       bluetoothAdapter.cancelDiscovery();

       try {
           // Connect to the remote device through the socket. This call blocks
           // until it succeeds or throws an exception.
           mmSocket.connect();
       } catch (IOException connectException) {
           // Unable to connect; close the socket and return.
           try {
               mmSocket.close();
           } catch (IOException closeException) {
               Log.e(TAG, "Could not close the client socket", closeException);
           }
           return;
       }

       // The connection attempt succeeded. Perform work associated with
       // the connection in a separate thread.
       manageMyConnectedSocket(mmSocket);
   }

   // Closes the client socket and causes the thread to finish.
   public void cancel() {
       try {
           mmSocket.close();
       } catch (IOException e) {
           Log.e(TAG, "Could not close the client socket", e);
       }
   }
}

لاحظ أنه في هذا المقتطف، يتم استدعاء cancelDiscovery() قبل محاولة الربط. يجب دائمًا الاتصال برقم cancelDiscovery() قبل connect()، خاصةً لأنّ نجاح "cancelDiscovery()" قد تم بنجاح بغض النظر عما إذا كانت عملية اكتشاف الأجهزة قيد التقدم حاليًا. إذا كان تطبيقك يحتاج إلى تحديد ما إذا كان اكتشاف الجهاز قيد التقدم، يمكنك التحقّق باستخدام isDiscovering().

إنّ طريقة manageMyConnectedSocket() الخاصة بالتطبيق مصمّمة لبدء سلسلة التعليمات الخاصة بنقل البيانات، وهي الطريقة التي تمت مناقشتها في القسم حول نقل بيانات البلوتوث.

عند الانتهاء من BluetoothSocket، اتصل دائمًا بـ close(). يؤدي ذلك إلى إغلاق المقبس المتصل على الفور وإطلاق جميع الموارد الداخلية ذات الصلة.