חיבור של מכשירי Bluetooth

כדי ליצור חיבור בין שני מכשירים, צריך להטמיע את שני סוגי המכשירים מנגנונים בצד השרת ובצד הלקוח כי מכשיר אחד חייב לפתוח שרת והשני צריך ליזום את החיבור באמצעות כתובת MAC. מכשיר השרת ומכשיר הלקוח משיגים כל אחד BluetoothSocket שונה דרכים שונות. השרת מקבל מידע על השקע כאשר יש חיבור נכנס התקבל. הלקוח מספק מידע על השקע כשהוא פותח ערוץ RFCOMM לשרת.

השרת והלקוח נחשבים כמחוברים זה לזה כאשר יש להם BluetoothSocket מחובר באותו ערוץ RFCOMM. בשלב הזה, כל שהמכשיר יכול לקבל זרמים של קלט ופלט, והעברת נתונים יכולה להתחיל, נדון בקטע שעוסק בהעברת Bluetooth . הקטע הזה מתארת איך להתחיל את החיבור בין שני מכשירים.

חשוב לוודא שיש לך הרשאות Bluetooth וגם צריך להגדיר את האפליקציה ל-Bluetooth לפני מתבצע ניסיון לאתר מכשירי Bluetooth.

טכניקות חיבור

אחת משיטות ההטמעה היא להכין כל מכשיר כשרת באופן אוטומטי. כך שלכל מכשיר יש שקע שרת פתוח שתומך בחיבורים. לחשבון במקרה כזה, כל אחד מהמכשירים יכול ליזום חיבור למכשיר השני ולהפוך הלקוח. לחלופין, מכשיר אחד יכול לארח באופן מפורש את החיבור ולפתוח שקע שרת על פי דרישה, והמכשיר השני מפעיל את החיבור.


איור 1. תיבת הדו-שיח של התאמת Bluetooth.

התחברות כשרת

כשרוצים לחבר שני מכשירים, אחד צריך לפעול כשרת על ידי החזקה של פתיחה BluetoothServerSocket המטרה של שקע השרת היא להאזין לבקשות חיבור נכנסות ולספק BluetoothSocket מקושר אחרי קבלת הבקשה. כאשר BluetoothSocket נרכש מ-BluetoothServerSocket, אפשר או צריך למחוק את BluetoothServerSocket, אלא אם רוצים את המכשיר כדי לקבל חיבורים נוספים.

כדי להגדיר שקע שרת ולאשר חיבור, מבצעים את הפעולות הבאות רצף פעולות:

  1. רוצה לקבל BluetoothServerSocket? listenUsingRfcommWithServiceRecord(String, UUID).

    המחרוזת היא שם ניתן לזיהוי של השירות שלכם, שאותו המערכת כותבת באופן אוטומטי ברשומת מסד נתונים חדשה של Service Discovery Protocol (SDP) במכשיר. השם שרירותי, והוא יכול להיות שם האפליקציה. המזהה הייחודי האוניברסלי (UUID) כלול גם הוא ברשומת ה-SDP והוא הבסיס להסכם החיבור עם מכשיר הלקוח. ש כלומר, כשהלקוח מנסה להתחבר למכשיר הזה, הוא כולל מזהה ייחודי אוניברסלי (UUID) שמזהה באופן ייחודי את השירות שאליו הוא רוצה להתחבר. האלה מזהי UUID חייבים להיות זהים כדי שהחיבור יאושר.

    UUID הוא פורמט סטנדרטי של 128 ביט למזהה מחרוזת שמשמש פרטי זיהוי. מזהה ייחודי אוניברסלי (UUID) משמש לזיהוי מידע ייחודי במערכת או ברשת, כי ההסתברות שמזהה ייחודי אוניברסלי (UUID) הוא שחוזרת על עצמה היא למעשה אפס. הוא נוצר באופן עצמאי, ללא שימוש של רשות מרכזית. במקרה הזה, הוא משמש כדי לזהות באופן ייחודי את שירות ה-Bluetooth של האפליקציה. כדי לקבל UUID לשימוש באפליקציה, אפשר להשתמש באחד מתוך כל התשובות האקראיות UUID גנרטורים באינטרנט, ואז לאתחל UUID עם fromString(String)

  2. ניתן להתחיל להאזין לבקשות חיבור באמצעות התקשרות accept()

    זו שיחה לחסימה. הוא חוזר כאשר מתקיים חיבור או במקרה של חריגה. חיבור יתקבל רק כאשר המכשיר המחובר לרשת אחרת שלח בקשת חיבור שמכילה מזהה ייחודי אוניברסלי (UUID) שתואם עם השקע הזה של שרת ההאזנה. כשהפעולה מצליחה, הפונקציה accept() מחזירה ערך BluetoothSocket מחובר.

  3. אלא אם ברצונך לאשר חיבורים נוספים, התקשר close()

    הפעלת ה-method הזו משחררת את שקע השרת ואת כל המשאבים שלו, לא סוגר את השדה 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(), בדיוק כמו במקרה שלך מצד הלקוח.

ה-method manageMyConnectedSocket() הספציפי לאפליקציה נועדה להפעיל את של העברת נתונים, כמו שהסברנו בנושא העברת Bluetooth .

בדרך כלל, עליך לסגור את BluetoothServerSocket בסיום להאזנה לחיבורים נכנסים. בדוגמה הזו, תתבצע קריאה אל close() מיד לאחר הצירוף של BluetoothSocket. אולי גם ברצונך לספק שם שיטה שיכולה לסגור את BluetoothSocket הפרטי באירוע שצריך להפסיק להאזין לו בשקע השרת הזה.

התחברות כלקוח

כדי להתחיל חיבור עם מכשיר מרוחק שתומך בשקע שרת פתוח, קודם צריך לקבל BluetoothDevice שמייצג את המכשיר המחובר לרשת אחרת. כדי ללמוד איך ליצור BluetoothDevice, למידע על מציאת Bluetooth מכשירים. צריך ואז להשתמש ב-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() היא קריאה לחסימה, תמיד צריך לבצע את הפעולה הזו תהליך החיבור בשרשור שנפרד מהפעילות הראשית (UI) של שרשור.

דוגמה

דוגמה בסיסית לשרשור של לקוח שמפעיל Bluetooth חיבור:

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

ה-method manageMyConnectedSocket() הספציפי לאפליקציה נועדה להפעיל את של העברת נתונים, כמו שהסברנו בקטע העברת נתוני Bluetooth.

כשמסיימים להשתמש ב-BluetoothSocket, צריך תמיד להתקשר ל-close(). אם עושים את זה סוגר באופן מיידי את השקע המחובר ומשאיר את כל השקעים הפנימיים הרלוונטיים במשאבי אנוש.