סקירה כללית על מארח ה-USB

כשהמכשיר מבוסס-Android נמצא במצב מארח USB, הוא משמש כמארח ה-USB, מפעיל את האוטובוס, וסופר את התקני ה-USB המחוברים. מצב מארח USB נתמך ב-Android מגרסה 3.1 ואילך.

סקירה כללית על API

לפני שמתחילים, חשוב להבין עם אילו כיתות צריך לעבוד. בטבלה הבאה מתוארים ממשקי ה-API של מארח ה-USB בחבילה android.hardware.usb.

טבלה 1. ממשקי API של מארח USB

דרגה תיאור
UsbManager הרשאה לספירת התקני USB מחוברים ולתקשר איתם.
UsbDevice מייצג מכשיר USB מחובר ומכיל שיטות לגישה מידע, ממשקים ונקודות קצה.
UsbInterface מייצג ממשק של התקן USB, שמגדיר קבוצה של פונקציונליות עבור במכשיר. מכשיר יכול לכלול ממשק אחד או יותר שבהם אפשר לתקשר.
UsbEndpoint מייצג נקודת קצה (endpoint) של ממשק, שהיא ערוץ תקשורת עבור הממשק הזה. יכולה להיות נקודת קצה (endpoint) אחת או יותר, ובדרך כלל יש בה נקודות קצה לקלט ולפלט תקשורת דו-כיוונית עם המכשיר.
UsbDeviceConnection מייצג חיבור למכשיר, שמעביר נתונים על נקודות קצה. הכיתה הזו מאפשרת לשלוח נתונים הלוך ושוב באופן סינכרוני או אסינכרוני.
UsbRequest מייצג בקשה אסינכרונית לתקשורת עם מכשיר דרך UsbDeviceConnection.
UsbConstants מגדירה קבועי USB שתואמים להגדרות ב-linux/usb/ch9.h של Linux. של הליבה.

ברוב המקרים, צריך להשתמש בכל הכיתות האלה (UsbRequest נדרש רק אם מבצעים תקשורת אסינכרונית). בזמן תקשורת עם התקן USB. באופן כללי, צריך לקבל UsbManager כדי לאחזר את ה-UsbDevice הרצוי. כשהמכשיר נמצא אצלך, עליך לאתר את ה-UsbInterface וה-UsbEndpoint המתאימים הממשק לתקשורת. לאחר קבלת נקודת הקצה הנכונה, פותחים UsbDeviceConnection כדי לתקשר עם התקן ה-USB.

דרישות לגבי מניפסטים ב-Android

ברשימה הבאה מפורטים מה צריך להוסיף לקובץ המניפסט של האפליקציה לפני עבודה עם ממשקי ה-API של מארח ה-USB:

  • מכיוון שלא כל המכשירים מבוססי Android יתמכו בממשקי API של מארח ה-USB, כוללים רכיב <uses-feature> שמצהיר שהאפליקציה משתמשת את התכונה android.hardware.usb.host.
  • צריך להגדיר את ערכת ה-SDK המינימלית של האפליקציה ל-API ברמה 12 ואילך. ממשקי ה-API של מארח ה-USB לא שקיימים ברמות ה-API המוקדמות יותר.
  • אם ברצונך שהאפליקציה שלך תקבל התראה על התקן USB מחובר, צריך לציין זוג אלמנטים של <intent-filter> ו-<meta-data> בשביל כוונת רכישה אחת (android.hardware.usb.action.USB_DEVICE_ATTACHED) בפעילות העיקרית שלך. הרכיב <meta-data> מפנה לקובץ משאב XML חיצוני עם הצהרה מידע מזהה על המכשיר שאותו אתם רוצים לזהות.

    בקובץ המשאבים בפורמט XML, מצהירים על רכיבי <usb-device> של ה-USB המכשירים שרוצים לסנן. הרשימה הבאה מתארת את המאפיינים של <usb-device> באופן כללי, צריך להשתמש במזהה הספק ובמזהה המוצר אם רוצים לסנן של מכשיר ספציפי, ולהשתמש במחלקה, בתת-מחלקה ובפרוטוקול אם רוצים לסנן לפי קבוצה של התקני USB, כמו התקני אחסון בנפח גדול או מצלמות דיגיטליות. אפשר לציין 'ללא' או את כל המאפיינים האלה. אם אין מאפיינים שתואמים לכל התקן USB, צריך לעשות רק אם האפליקציה שלכם דורשת זאת:

    • vendor-id
    • product-id
    • class
    • subclass
    • protocol (מכשיר או ממשק)

    שומרים את קובץ המשאב בספרייה res/xml/. השם של קובץ המשאב (ללא הסיומת .xml) צריכה להיות זהה לזו שציינתם רכיב <meta-data>. הפורמט של קובץ המשאבים בפורמט XML הוא דוגמה שלמטה.

דוגמאות לקובצי מניפסט ולקובצי משאבים

בדוגמה הבאה תוכלו לראות מניפסט לדוגמה ואת קובץ המשאבים התואם שלו:

<manifest ...>
    <uses-feature android:name="android.hardware.usb.host" />
    <uses-sdk android:minSdkVersion="12" />
    ...
    <application>
        <activity ...>
            ...
            <intent-filter>
                <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
            </intent-filter>

            <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
                android:resource="@xml/device_filter" />
        </activity>
    </application>
</manifest>

במקרה כזה, צריך לשמור את קובץ המשאבים הבא ב- res/xml/device_filter.xml שמציין שכל התקן USB עם יש לסנן את המאפיינים:

<?xml version="1.0" encoding="utf-8"?>

<resources>
    <usb-device vendor-id="1234" product-id="5678" class="255" subclass="66" protocol="1" />
</resources>

עבודה עם מכשירים

כשמשתמשים מחברים התקני USB למכשיר מבוסס Android, מערכת Android יכולה לקבוע האם האפליקציה שלך מתעניינת במכשיר המחובר. אם כן, אפשר להגדיר בתקשורת עם המכשיר, אם יש צורך. כדי לעשות זאת, האפליקציה צריכה:

  1. לגלות התקני USB מחוברים באמצעות מסנן Intent לקבלת התראה כשהמשתמש שמחבר התקן USB או על ידי ספירת התקני USB שכבר מחוברים.
  2. צריך לבקש מהמשתמש הרשאה להתחבר להתקן ה-USB, אם הוא עדיין לא התקבל.
  3. תקשורת עם התקן ה-USB על ידי קריאה וכתיבה של נתונים בממשק המתאים נקודות קצה (endpoints).

היכרות עם מכשיר

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

שימוש במסנן Intent

כדי שהאפליקציה שלך תאתר התקן USB ספציפי, אפשר לציין מסנן Intent כדי לסנן לפי ה-Intent android.hardware.usb.action.USB_DEVICE_ATTACHED. יחד עם את מסנן ה-Intent הזה, עליכם לציין קובץ משאבים שמציין את המאפיינים של ה-USB המכשיר, כגון מזהה המוצר ומזהה הספק. כשמשתמשים מחברים מכשיר שתואם למכשיר שלכם מסנן, המערכת מציגה להם תיבת דו-שיח ששואלת אם הם רוצים להפעיל את האפליקציה. אם המשתמשים יאשרו את ההזמנה, לאפליקציה יש הרשאה אוטומטית לגשת למכשיר עד שהמכשיר מנותק.

הדוגמה הבאה מראה איך להצהיר על מסנן Intent:

<activity ...>
...
    <intent-filter>
        <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
    </intent-filter>

    <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
        android:resource="@xml/device_filter" />
</activity>

הדוגמה הבאה מראה איך להצהיר על קובץ המשאב התואם שמציין את התקני USB שמעניינים אותך:

<?xml version="1.0" encoding="utf-8"?>

<resources>
    <usb-device vendor-id="1234" product-id="5678" />
</resources>

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

Kotlin

val device: UsbDevice? = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)

Java

UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);

ספירת מכשירים

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

Kotlin

val manager = getSystemService(Context.USB_SERVICE) as UsbManager
...
val deviceList = manager.getDeviceList()
val device = deviceList.get("deviceName")

Java

UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);
...
HashMap<String, UsbDevice> deviceList = manager.getDeviceList();
UsbDevice device = deviceList.get("deviceName");

אם רוצים, אפשר גם לקבל איטרטור ממפת הגיבוב (hash) ולעבד כל מכשיר בנפרד אחד:

Kotlin

val manager = getSystemService(Context.USB_SERVICE) as UsbManager
..
val deviceList: HashMap<String, UsbDevice> = manager.deviceList
deviceList.values.forEach { device ->
    // your code
}

Java

UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);
...
HashMap<String, UsbDevice> deviceList = manager.getDeviceList();
Iterator<UsbDevice> deviceIterator = deviceList.values().iterator();
while(deviceIterator.hasNext()){
    UsbDevice device = deviceIterator.next();
    // your code
}

קבלת הרשאה לתקשורת עם מכשיר

לפני התקשורת עם התקן ה-USB, האפליקציה צריכה לקבל הרשאה משתמשים.

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

ייתכן שיהיה צורך לבקש הרשאה מפורשת במצבים מסוימים, למשל האפליקציה סופרת התקני USB שכבר מחוברים ולאחר מכן רוצים לתקשר אחת. צריך לבדוק אם יש הרשאת גישה למכשיר לפני שמנסים לתקשר איתו. אם המיקום לא, תקבלו שגיאת זמן ריצה אם המשתמש דחה את ההרשאה לגשת למכשיר.

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

Kotlin

private const val ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION"

private val usbReceiver = object : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        if (ACTION_USB_PERMISSION == intent.action) {
            synchronized(this) {
                val device: UsbDevice? = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)

                if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                    device?.apply {
                        // call method to set up device communication
                    }
                } else {
                    Log.d(TAG, "permission denied for device $device")
                }
            }
        }
    }
}

Java

private static final String ACTION_USB_PERMISSION =
    "com.android.example.USB_PERMISSION";
private final BroadcastReceiver usbReceiver = new BroadcastReceiver() {

    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (ACTION_USB_PERMISSION.equals(action)) {
            synchronized (this) {
                UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);

                if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                    if(device != null){
                      // call method to set up device communication
                   }
                }
                else {
                    Log.d(TAG, "permission denied for device " + device);
                }
            }
        }
    }
};

כדי לרשום את מקלט השידורים, צריך להוסיף זאת בשיטת onCreate() ב פעילות:

Kotlin

private const val ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION"
...
val manager = getSystemService(Context.USB_SERVICE) as UsbManager
...
permissionIntent = PendingIntent.getBroadcast(this, 0,
                  Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_IMMUTABLE)
val filter = IntentFilter(ACTION_USB_PERMISSION)
registerReceiver(usbReceiver, filter)

Java

UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
private static final String ACTION_USB_PERMISSION =
    "com.android.example.USB_PERMISSION";
...
permissionIntent = PendingIntent.getBroadcast(this, 0,
              new Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_IMMUTABLE);
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
registerReceiver(usbReceiver, filter);

כדי להציג את תיבת הדו-שיח שמבקשת מהמשתמשים הרשאה להתחבר למכשיר, קוראים לשיטה requestPermission():

Kotlin

lateinit var device: UsbDevice
...
usbManager.requestPermission(device, permissionIntent)

Java

UsbDevice device;
...
usbManager.requestPermission(device, permissionIntent);

כשמשתמשים משיבים לתיבת הדו-שיח, מקלט השידור מקבל את הכוונה שמכילה את EXTRA_PERMISSION_GRANTED נוסף, שהוא ערך בוליאני שמייצג את התשובה. צריך לבדוק את הערך הנוסף הזה כ-TRUE לפני החיבור אל במכשיר.

תקשורת באמצעות מכשיר

התקשורת עם התקן USB יכולה להיות סינכרונית או אסינכרונית. בכל מקרה, ליצור שרשור חדש שבו יבצע את כל העברות הנתונים, כך שלא תחסום את שרשור בממשק המשתמש. כדי להגדיר כראוי תקשורת עם מכשיר, צריך UsbInterface וגם UsbEndpoint מתוך המכשיר שברצונך לתקשר איתו ולשלוח בקשות בנקודת הקצה הזו באמצעות UsbDeviceConnection. באופן כללי, הקוד צריך:

  • בדיקת המאפיינים של אובייקט UsbDevice, כמו מזהה מוצר, מזהה ספק או סיווג מכשיר כדי להבין אם אתם רוצים לתקשר עם במכשיר.
  • כאשר אתה בטוח שברצונך לתקשר עם המכשיר, חפש את UsbInterface שרוצים להשתמש בהם כדי לתקשר עם UsbEndpoint המתאים בממשק הזה. לממשקים אפשר להגדיר אחד או יותר, בדרך כלל תהיה נקודת קצה (endpoint) דו-כיוונית של קלט ופלט תקשורת בלתי מכוונת.
  • כשמוצאים את נקודת הקצה הנכונה, פותחים UsbDeviceConnection בנקודת הקצה הזאת.
  • מספקים את הנתונים שרוצים לשדר בנקודת הקצה באמצעות השיטה bulkTransfer() או controlTransfer(). אתם צריכים לבצע את השלב הזה בשרשור אחר כדי למנוע חסימה של ה-thread הראשי של ממשק המשתמש. לקבלת מידע נוסף למידע על השימוש בשרשורים ב-Android, ראו תהליכים שרשורים.

קטע הקוד הבא הוא דרך טריוויאלית לבצע העברת נתונים סינכרונית. הקוד שלך צריכה להיות יותר לוגיקה כדי למצוא בצורה נכונה את הממשק ונקודות הקצה הנכונים לתקשורת וגם לבצע העברת נתונים בשרשור שונה מה-thread הראשי של ממשק המשתמש:

Kotlin

private lateinit var bytes: ByteArray
private val TIMEOUT = 0
private val forceClaim = true

...

device?.getInterface(0)?.also { intf ->
    intf.getEndpoint(0)?.also { endpoint ->
        usbManager.openDevice(device)?.apply {
            claimInterface(intf, forceClaim)
            bulkTransfer(endpoint, bytes, bytes.size, TIMEOUT) //do in another thread
        }
    }
}

Java

private Byte[] bytes;
private static int TIMEOUT = 0;
private boolean forceClaim = true;

...

UsbInterface intf = device.getInterface(0);
UsbEndpoint endpoint = intf.getEndpoint(0);
UsbDeviceConnection connection = usbManager.openDevice(device);
connection.claimInterface(intf, forceClaim);
connection.bulkTransfer(endpoint, bytes, bytes.length, TIMEOUT); //do in another thread

כדי לשלוח נתונים באופן אסינכרוני, צריך להשתמש במחלקה UsbRequest אל initialize וב-queue לבקשה אסינכרונית, ואז להמתין לתוצאה עם requestWait().

סיום התקשורת עם מכשיר

בסיום התקשורת עם מכשיר או אם המכשיר נותק, צריך לסגור את UsbInterface ואת UsbDeviceConnection באמצעות התקשרות אל releaseInterface() ו close() כדי להאזין לאירועים שמנותקים: יוצרים מקלט שידורים כמו בדוגמה הבאה:

Kotlin

var usbReceiver: BroadcastReceiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {

        if (UsbManager.ACTION_USB_DEVICE_DETACHED == intent.action) {
            val device: UsbDevice? = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)
            device?.apply {
                // call your method that cleans up and closes communication with the device
            }
        }
    }
}

Java

BroadcastReceiver usbReceiver = new BroadcastReceiver() {
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();

      if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
            UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
            if (device != null) {
                // call your method that cleans up and closes communication with the device
            }
        }
    }
};

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