متصل کردن دستگاه‌های بلوتوث

برای ایجاد اتصال بین دو دستگاه، باید هم سازوکار سمت سرور و هم سازوکار سمت کارخواه را پیاده‌سازی کنید، زیرا یک دستگاه باید سوکت سرور را باز کند و دستگاه دیگر باید اتصال را بااستفاده از مک آدرس دستگاه سرور آغاز کند. دستگاه سرور و دستگاه کلاینت هرکدام BluetoothSocket لازم را به روش‌های مختلفی دریافت می‌کنند. وقتی اتصال ورودی پذیرفته می‌شود، سرور اطلاعات سوکت را دریافت می‌کند. وقتی کارخواه کانال RFCOMM را به‌سمت سرور باز می‌کند، اطلاعات سوکت را ارائه می‌دهد.

سرور و کارخواه زمانی متصل به یکدیگر درنظر گرفته می‌شوند که هرکدام یک BluetoothSocket متصل در همان کانال RFCOMM داشته باشند. در این مرحله، هر دستگاه می‌تواند جریان‌های ورودی و خروجی را دریافت کند و انتقال داده‌ها آغاز شود، که در بخش مربوط به انتقال داده‌های بلوتوث بحث شده است. این بخش نحوه شروع اتصال بین دو دستگاه را شرح می‌دهد.

قبل‌از تلاش برای پیدا کردن دستگاه‌های بلوتوث، مطمئن شوید که اجازه‌های بلوتوث مناسب را دارید و برنامه‌تان را برای بلوتوث راه‌اندازی کرده‌اید.

تکنیک‌های اتصال

یکی از تکنیک‌های پیاده‌سازی این است که هر دستگاه به‌طور خودکار به‌عنوان سرور آماده شود تا هر دستگاه یک سوکت سرور باز داشته باشد و به اتصالات گوش دهد. در این حالت، هریک از دستگاه‌ها می‌تواند اتصال با دستگاه دیگر را آغاز کند و به کارخواه تبدیل شود. یا اینکه یک دستگاه می‌تواند به‌طور صریح اتصال را میزبانی کند و سوکت سرور را درصورت درخواست باز کند، و دستگاه دیگر اتصال را آغاز کند.


شکل ۱. کادر گفتگوی مرتبط‌سازی بلوتوث.

متصل شدن به‌عنوان سرور

وقتی می‌خواهید دو دستگاه را متصل کنید، یکی باید با نگه داشتن BluetoothServerSocket باز، به‌عنوان سرور عمل کند. هدف سوکت سرور گوش دادن به درخواست‌های اتصال ورودی و ارائه BluetoothSocket متصل‌شده پس‌از پذیرفته شدن درخواست است. وقتی BluetoothSocket از BluetoothServerSocket دریافت می‌شود، BluetoothServerSocket می‌تواند—و باید—دور انداخته شود، مگراینکه بخواهید دستگاه اتصالات بیشتری را بپذیرد.

برای راه‌اندازی سوکت سرور و پذیرش اتصال، توالی مراحل زیر را تکمیل کنید:

  1. با تماس با listenUsingRfcommWithServiceRecord(String, UUID)، BluetoothServerSocket دریافت کنید.

    رشته نام قابل‌شناسایی سرویس شما است که سیستم به‌طور خودکار آن را در ورودی پایگاه داده جدید «پروتکل کشف سرویس» (SDP) در دستگاه می‌نویسد. این نام اختیاری است و می‌تواند به‌سادگی نام برنامه شما باشد. «شناسه یکتای همگانی» (UUID) نیز در ورودی SDP گنجانده شده است و مبنای توافق اتصال با دستگاه کارخواه را تشکیل می‌دهد. یعنی وقتی کارخواه تلاش می‌کند با این دستگاه متصل شود، UUID را به‌همراه دارد که به‌طور منحصربه‌فرد سرویسی را که می‌خواهد به آن متصل شود شناسایی می‌کند. این UUIDها باید مطابقت داشته باشند تا اتصال پذیرفته شود.

    UUID یک قالب استاندارد ۱۲۸ بیتی برای شناسه رشته‌ای است که برای شناسایی یکتای اطلاعات استفاده می‌شود. از UUID برای شناسایی اطلاعاتی استفاده می‌شود که باید در سیستم یا شبکه منحصربه‌فرد باشد، زیرا احتمال تکرار UUID عملاً صفر است. این کلید به‌طور مستقل و بدون استفاده از مرجع متمرکز تولید می‌شود. در این مورد، از آن برای شناسایی منحصربه‌فرد سرویس بلوتوث برنامه‌تان استفاده می‌شود. برای دریافت UUID برای استفاده با برنامه‌تان، می‌توانید از یکی از مولدهای تصادفی متعدد UUID در وب استفاده کنید، سپس UUID را با fromString(String) مقداردهی اولیه کنید.

  2. با فراخوانی accept()، گوش دادن به درخواست‌های اتصال را شروع کنید.

    این تماس مسدودکننده است. وقتی اتصالی پذیرفته شود یا استثنایی رخ دهد، برمی‌گردد. اتصال فقط زمانی پذیرفته می‌شود که دستگاه ازراه‌دور درخواست اتصالی حاوی UUID ارسال کرده باشد که با UUID ثبت‌شده در این سوکت سرور شنونده مطابقت داشته باشد. وقتی موفقیت‌آمیز باشد، accept() BluetoothSocket متصل را برمی‌گرداند.

  3. مگر اینکه بخواهید اتصالات بیشتری را بپذیرید، با close() تماس بگیرید.

    این فراخوانی روش، سوکت سرور و همه منابع آن را آزاد می‌کند، اما BluetoothSocket متصل‌شده را که توسط accept() برگردانده شده است نمی‌بندد. برخلاف TCP/IP،‏ RFCOMM در هر زمان فقط یک کارخواه متصل را در هر کانال مجاز می‌کند، بنابراین در بیشتر موارد منطقی است که بلافاصله پس‌از پذیرش سوکت متصل، close() را در BluetoothServerSocket فراخوانی کنید.

چون فراخوانی accept() یک فراخوانی مسدودکننده است، آن را در رشته میانای کاربر فعالیت اصلی اجرا نکنید. اجرای آن در رشته‌ای دیگر تضمین می‌کند که برنامه‌تان همچنان می‌تواند به دیگر تعاملات کاربر پاسخ دهد. معمولاً منطقی است که همه کارهایی که شامل BluetoothServerSocket یا BluetoothSocket می‌شود در رشته جدیدی که توسط برنامه‌تان مدیریت می‌شود انجام شود. برای لغو کردن تماس مسدودشده‌ای مثل accept()، با close() در BluetoothServerSocket یا BluetoothSocket از رشته دیگری تماس بگیرید. توجه داشته باشید که همه روش‌ها در BluetoothServerSocket یا BluetoothSocket ازنظر رشته‌ای ایمن هستند.

مثال

در زیر رشته ساده‌شده‌ای برای مؤلفه سرور که اتصالات ورودی را می‌پذیرد آورده شده است:

کاتلین

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)
       }
   }
}

جاوا

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، با تماس با createRfcommSocketToServiceRecord(UUID)، BluetoothSocket دریافت کنید.

    این روش یک شیء BluetoothSocket را مقداردهی اولیه می‌کند که به مشتری اجازه می‌دهد به BluetoothDevice متصل شود. UUID ارسال‌شده در اینجا باید با UUID استفاده‌شده توسط دستگاه سرور هنگام فراخوانی listenUsingRfcommWithServiceRecord(String, UUID) برای باز کردن BluetoothServerSocket آن مطابقت داشته باشد. برای استفاده از UUID منطبق، رشته UUID را در برنامه‌تان کدگذاری ثابت کنید و سپس آن را از کد سرور و مشتری ارجاع دهید.

  2. با تماس با connect()، اتصال را برقرار کنید. توجه داشته باشید که این روش یک تماس مسدودکننده است.

    پس‌از اینکه کارخواه این روش را فراخوانی می‌کند، سیستم جستجوی SDP را برای یافتن دستگاه ازراه‌دور با شناسه UUID منطبق انجام می‌دهد. اگر جستجو موفقیت‌آمیز باشد و دستگاه ازراه‌دور اتصال را بپذیرد، کانال RFCOMM را برای استفاده درطول اتصال هم‌رسانی می‌کند و روش connect() برمی‌گردد. اگر اتصال برقرار نشود، یا اگر زمان روش connect() به‌پایان برسد (پس‌از حدود ۱۲ ثانیه)، روش IOException را برمی‌گرداند.

چون connect() یک فراخوانی مسدودکننده است، همیشه باید این روال اتصال را در رشته‌ای جدا از رشته فعالیت اصلی (میانای کاربر) انجام دهید.

مثال

در زیر نمونه‌ای ساده از رشته کلاینت که اتصال بلوتوث را آغاز می‌کند آورده شده است:

کاتلین

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)
       }
   }
}

جاوا

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() تماس بگیرید. با این کار، سوکت متصل‌شده فوراً بسته می‌شود و همه منابع داخلی مرتبط آزاد می‌شود.