الوصول إلى أجهزة USB على الويب

تساعد واجهة برمجة تطبيقات WebUSB على جعل USB أكثر أمانًا وسهولة من خلال توفيره على الويب.

François Beaufort
François Beaufort

إذا قلت "USB" بمنتهى الوضوح، فهناك فرصة جيدة أن تفكر على الفور في لوحات المفاتيح وأجهزة الماوس وأجهزة الصوت والفيديو وأجهزة التخزين. أنت على حق، ولكن ستجد أنواعًا أخرى من أجهزة الناقل التسلسلي العالمي (USB) هناك.

تتطلب أجهزة USB غير الموحّدة هذه من موردي الأجهزة كتابة برامج تشغيل وحزم تطوير برامج (SDK) خاصة بالنظام الأساسي حتى تتمكن أنت (مطوّر البرامج) من الاستفادة منها. للأسف، أدى هذا الرمز الخاص بالنظام الأساسي في السابق إلى منع استخدام هذه الأجهزة على الويب. وهذا أحد أسباب إنشاء واجهة برمجة تطبيقات WebUSB API، وهو توفير طريقة لعرض خدمات أجهزة USB على الويب. باستخدام واجهة برمجة التطبيقات هذه، سيتمكن مصنِّعو الأجهزة من إنشاء حزم SDK عبر الأنظمة الأساسية لأجهزتهم.

والأهم من ذلك أنّه سيجعل USB أكثر أمانًا وأسهل في الاستخدام من خلال توفيره على الويب.

لنطّلع على السلوك الذي يمكنك توقّعه عند استخدام WebUSB API:

  1. شراء جهاز USB.
  2. يُرجى توصيله بجهاز الكمبيوتر. يظهر إشعار على الفور، مع موقع الويب الصحيح للانتقال إليه لهذا الجهاز.
  3. انقر على الإشعار. الموقع موجود وجاهز للاستخدام!
  4. انقر للاتصال وسيظهر أداة اختيار جهاز USB في Chrome حيث يمكنك اختيار جهازك.

مفاجأة!

كيف سيكون هذا الإجراء بدون واجهة برمجة تطبيقات WebUSB؟

  1. تثبيت تطبيق خاص بالنظام الأساسي.
  2. إذا كان التطبيق متوافقًا مع نظام التشغيل الذي أستخدمه، تحقّق من أنّني قمت بتنزيل الشيء الصحيح.
  3. تثبيت الشيء. إذا كنت محظوظًا، فلن تحصل على مطالبات مخيفة من نظام التشغيل أو نوافذ منبثقة تحذّرك بشأن تثبيت برامج التشغيل/التطبيقات من الإنترنت. إذا لم يحالفك الحظ، فإن برامج التشغيل أو التطبيقات المثبتة تعطلت وتؤدي إلى إلحاق الضرر بجهاز الكمبيوتر. (تذكَّر أنّ شبكة الويب مصمّمة لاحتواء مواقع إلكترونية معطّلة).
  4. إذا استخدمت الميزة مرة واحدة فقط، سيظل الرمز على جهاز الكمبيوتر حتى تفكر في إزالتها. (على الويب، يتم في النهاية استعادة المساحة غير المستخدمة).

قبل البدء

تفترض هذه المقالة أنّ لديك بعض المعرفة الأساسية حول آلية عمل USB. وإلا، أنصحك بقراءة USB in a NutShell. للحصول على معلومات أساسية حول USB، يمكنك الاطّلاع على مواصفات USB الرسمية.

تتوفّر WebUSB API في إصدار Chrome 61.

متاحة لمرحلة التجربة والتقييم

للحصول على أكبر قدر ممكن من الملاحظات من المطوّرين الذين يستخدمون WebUSB API في هذا المجال، أضفنا هذه الميزة سابقًا في الإصدارين 54 وChrome 57 من Chrome كمرحلة تجريبية.

انتهت الفترة التجريبية الأخيرة بنجاح في أيلول (سبتمبر) 2017.

الخصوصية والأمان

HTTPS فقط

وبسبب قدرة هذه الميزة، لا تعمل إلا مع السياقات الآمنة. وهذا يعني أنك ستحتاج إلى الإنشاء مع وضع بروتوكول أمان طبقة النقل (TLS) في الاعتبار.

يجب تفعيل إيماءة المستخدم.

كإجراء أمني وقائي، لا يمكن الاتصال بـ navigator.usb.requestDevice() إلا من خلال إيماءة المستخدم مثل اللمس أو النقر بالماوس.

سياسة الأذونات

سياسة الأذونات هي آلية تتيح للمطوّرين تفعيل وإيقاف العديد من ميزات المتصفّح وواجهات برمجة التطبيقات بشكل انتقائي. يمكن تحديده عن طريق عنوان HTTP و/أو سمة "allow" في إطار iframe.

يمكنك تحديد سياسة أذونات تتحكّم في ما إذا كانت السمة usb تظهر على عنصر المستكشف، أو بعبارة أخرى في حال السماح باستخدام WebUSB.

في ما يلي مثال على سياسة عنوان لا يُسمح فيها باستخدام WebUSB:

Feature-Policy: fullscreen "*"; usb "none"; payment "self" https://payment.example.com

في ما يلي مثال آخر على سياسة حاويات حيث يُسمح باستخدام أجهزة USB:

<iframe allowpaymentrequest allow="usb; fullscreen"></iframe>

لنبدأ الترميز

تعتمد WebUSB API بشكل كبير على وعود JavaScript. يمكنك الاطّلاع على هذا البرنامج التعليمي حول التعهدات إذا لم تكن معتادًا على ذلك. هناك أمر آخر، () => {} هو ببساطة دوال الأسهم لعام 2015 في ECMAScript.

الوصول إلى أجهزة USB

يمكنك الطلب من المستخدم اختيار جهاز USB واحد متصل باستخدام navigator.usb.requestDevice() أو الاتصال بـ navigator.usb.getDevices() للحصول على قائمة بجميع أجهزة USB المتصلة التي تم منح الموقع الإلكتروني إذن الوصول إليها.

تستخدم الدالة navigator.usb.requestDevice() كائن JavaScript إلزاميًا يعرّف filters. تُستخدَم هذه الفلاتر لمطابقة أي جهاز USB مع المورِّد المحدد (vendorId) ومعرّفات المنتج (productId) إذا أردت. ويمكن أيضًا تحديد المفاتيح classCode وprotocolCode وserialNumber وsubclassCode.

لقطة شاشة لإشعار مستخدم جهاز USB في Chrome
رسالة مطالبة لمستخدم جهاز USB:

على سبيل المثال، في ما يلي طريقة الوصول إلى جهاز Arduino المرتبط الذي تم إعداده للسماح بالوصول إلى المصدر.

navigator.usb.requestDevice({ filters: [{ vendorId: 0x2341 }] })
.then(device => {
  console.log(device.productName);      // "Arduino Micro"
  console.log(device.manufacturerName); // "Arduino LLC"
})
.catch(error => { console.error(error); });

قبل أن تسأل، لم أتوصل إلى هذا الرقم السداسي العشري 0x2341 بشكل سحري. لقد بحثت عن كلمة "Arduino" في قائمة معرّفات USB هذه.

يتضمن جهاز USB device الذي تم إرجاعه وفقًا لما تم توفيره أعلاه بعض المعلومات الأساسية والمهمة في الوقت ذاته حول الجهاز، مثل إصدار USB المتوافق، والحد الأقصى لحجم الحزمة، ومعرّف المورّد، ومعرّفات المنتجات، وعدد الإعدادات المحتملة التي يمكن أن يمتلكها الجهاز. وهي تحتوي في الأساس على جميع الحقول المضمّنة في أداة وصف USB للجهاز.

// Get all connected USB devices the website has been granted access to.
navigator.usb.getDevices().then(devices => {
  devices.forEach(device => {
    console.log(device.productName);      // "Arduino Micro"
    console.log(device.manufacturerName); // "Arduino LLC"
  });
})

بالمناسبة، إذا أعلن جهاز USB عن توافقه مع WebUSB، بالإضافة إلى تحديد عنوان URL للصفحة المقصودة، سيعرض Chrome إشعارًا مستمرًا عند توصيل جهاز USB. سيؤدي النقر على هذا الإشعار إلى فتح الصفحة المقصودة.

لقطة شاشة لإشعار WebUSB في Chrome
إشعار WebUSB:

التحدّث إلى لوحة Arduino USB

حسنًا، لنرَ الآن مدى سهولة الاتصال بلوحة Aruino متوافقة مع WebUSB عبر منفذ USB. يمكنك الاطّلاع على التعليمات الواردة على الرابط https://github.com/webusb/arduino حول تفعيل رسوماتك على WebUSB.

لا داعي للقلق، فسأتناول جميع طرق أجهزة WebUSB المذكورة أدناه في هذه المقالة.

let device;

navigator.usb.requestDevice({ filters: [{ vendorId: 0x2341 }] })
.then(selectedDevice => {
    device = selectedDevice;
    return device.open(); // Begin a session.
  })
.then(() => device.selectConfiguration(1)) // Select configuration #1 for the device.
.then(() => device.claimInterface(2)) // Request exclusive control over interface #2.
.then(() => device.controlTransferOut({
    requestType: 'class',
    recipient: 'interface',
    request: 0x22,
    value: 0x01,
    index: 0x02})) // Ready to receive data
.then(() => device.transferIn(5, 64)) // Waiting for 64 bytes of data from endpoint #5.
.then(result => {
  const decoder = new TextDecoder();
  console.log('Received: ' + decoder.decode(result.data));
})
.catch(error => { console.error(error); });

تجدر الإشارة إلى أنّ مكتبة WebUSB التي أستخدمها تعمل فقط على تنفيذ نموذج واحد من البروتوكول (استنادًا إلى بروتوكول USB التسلسلي القياسي) وأنّه بإمكان المُصنِّعين إنشاء أي مجموعة وأنواع من نقاط النهاية يريدونها. تُعد عمليات نقل التحكم رائعة بشكل خاص لأوامر التكوين الصغيرة لأنها تحصل على أولوية الحافلة ولها هيكل محدد جيدًا.

وإليك الرسم الذي تم تحميله على لوح Arduino.

// Third-party WebUSB Arduino library
#include <WebUSB.h>

WebUSB WebUSBSerial(1 /* https:// */, "webusb.github.io/arduino/demos");

#define Serial WebUSBSerial

void setup() {
  Serial.begin(9600);
  while (!Serial) {
    ; // Wait for serial port to connect.
  }
  Serial.write("WebUSB FTW!");
  Serial.flush();
}

void loop() {
  // Nothing here for now.
}

تؤدي مكتبة WebUSB Arduino التابعة لجهة خارجية والمستخدَمة في نموذج الرمز البرمجي أعلاه إلى تنفيذ أمرين أساسيين:

  • يعمل الجهاز كجهاز WebUSB يمكِّن Chrome من قراءة عنوان URL للصفحة المقصودة.
  • يؤدي هذا الإجراء إلى عرض واجهة WebUSB Serial API التي يمكنك استخدامها لإلغاء الواجهة التلقائية.

انظر إلى رمز JavaScript مرة أخرى. بعد حصول المستخدم على device، يجري device.open() جميع الخطوات الخاصة بالنظام الأساسي لبدء جلسة باستخدام جهاز USB. بعد ذلك، عليّ اختيار إعداد USB مع device.selectConfiguration(). تذكّر أنّ الإعدادات تحدّد طريقة تشغيل الجهاز، والحدّ الأقصى لاستهلاكه للطاقة وعدد واجهاته. في ما يتعلق بالواجهات، أريد أيضًا أن أطلب الحصول على إذن وصول حصري إلى device.claimInterface()، لأنّه لا يمكن نقل البيانات إلى واجهة أو نقاط نهاية مرتبطة إلا عند المطالبة بالواجهة. أخيرًا، يجب إرسال طلب device.controlTransferOut() لإعداد جهاز Arduino باستخدام الأوامر المناسبة للتواصل من خلال WebUSB Serial API.

ومن هناك، ينفِّذ device.transferIn() عملية نقل مجمّعة إلى الجهاز لإبلاغه بأنّ المضيف جاهز لتلقّي البيانات المجمّعة. بعد ذلك، يتم تنفيذ الوعد من خلال استخدام عنصر result يحتوي على data من DataView يجب تحليله بشكل مناسب.

إذا كنت معتادًا على استخدام USB، فمن المفترض أن يبدو كل هذا مألوفًا جدًا.

أريد المزيد

تتيح لك WebUSB API التفاعل مع جميع أنواع نقاط النهاية/النقل عبر USB:

  • تتم معالجة عمليات النقل CONTROL، التي يتم استخدامها لإرسال أو تلقّي معلَمات الإعدادات أو الأوامر إلى جهاز USB، باستخدام controlTransferIn(setup, length) وcontrolTransferOut(setup, data).
  • تُستخدم عمليات النقل المتقطعة، التي تُستخدم لفترة قصيرة من البيانات الحساسة، بالطرق نفسها التي تتم بها عمليات النقل المجمّعة مع transferIn(endpointNumber, length) وtransferOut(endpointNumber, data).
  • يتم التعامل مع عمليات نقل البيانات ISO CHRONOUS المستخدمة لتدفقات البيانات مثل الفيديو والصوت باستخدام isochronousTransferIn(endpointNumber, packetLengths) وisochronousTransferOut(endpointNumber, data, packetLengths).
  • تُستخدم عمليات النقل المجمّعة لنقل كمية كبيرة من البيانات غير الحساسة للوقت بطريقة موثوقة، وذلك باستخدام transferIn(endpointNumber, length) وtransferOut(endpointNumber, data).

ننصحك أيضًا بإلقاء نظرة على مشروع WebLight الذي يصمّمه "مايك تساو" والذي يقدّم مثالاً أوليًا لتصميم جهاز LED يتم التحكّم فيه عبر USB ومصمم لواجهة برمجة تطبيقات WebUSB API (وليس باستخدام Arduino هنا). ستجد الأجهزة والبرامج والبرامج الثابتة.

إبطال الوصول إلى جهاز USB

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

// Voluntarily revoke access to this USB device.
await device.forget();

بسبب توفّر forget() في Chrome 101 أو الإصدارات الأحدث، تحقَّق مما إذا كانت هذه الميزة متوافقة مع ما يلي:

if ("usb" in navigator && "forget" in USBDevice.prototype) {
  // forget() is supported.
}

الحدود القصوى المسموح بها على حجم عمليات النقل

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

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

const BULK_TRANSFER_SIZE = 16 * 1024; // 16KB
const MAX_NUMBER_TRANSFERS = 3;

async function sendRawPayload(device, endpointNumber, data) {
  let i = 0;
  let pendingTransfers = [];
  let remainingBytes = data.byteLength;
  while (remainingBytes > 0) {
    const chunk = data.subarray(
      i * BULK_TRANSFER_SIZE,
      (i + 1) * BULK_TRANSFER_SIZE
    );
    // If we've reached max number of transfers, let's wait.
    if (pendingTransfers.length == MAX_NUMBER_TRANSFERS) {
      await pendingTransfers.shift();
    }
    // Submit transfers that will be executed in order.
    pendingTransfers.push(device.transferOut(endpointNumber, chunk));
    remainingBytes -= chunk.byteLength;
    i++;
  }
  // And wait for last remaining transfers to complete.
  await Promise.all(pendingTransfers);
}

نصائح

أصبح تصحيح أخطاء USB في Chrome أسهل من خلال الصفحة الداخلية about://device-log التي تتيح لك الاطّلاع على جميع الأحداث المتعلّقة بأجهزة USB في مكان واحد.

لقطة شاشة لصفحة سجلّ الجهاز لتصحيح أخطاء WebUSB في Chrome
صفحة سجلّ الجهاز في Chrome لتصحيح الأخطاء في WebUSB API

ويمكن أيضًا الاستعانة بالصفحة الداخلية about://usb-internals، كما تسمح لك بمحاكاة الاتصال بأجهزة WebUSB الافتراضية وانقطاع الاتصال بها. ويكون هذا مفيدًا لإجراء اختبار واجهة المستخدم بدون أجهزة حقيقية.

لقطة شاشة للصفحة الداخلية لتصحيح أخطاء WebUSB في Chrome
الصفحة الداخلية في Chrome لتصحيح الأخطاء في WebUSB API

في معظم أنظمة Linux، يتم تلقائيًا ربط أجهزة USB بأذونات القراءة فقط. للسماح لمتصفِّح Chrome بفتح جهاز USB، ستحتاج إلى إضافة قاعدة udev جديدة. أنشئ ملفًا في "/etc/udev/rules.d/50-yourdevicename.rules" يتضمّن المحتوى التالي:

SUBSYSTEM=="usb", ATTR{idVendor}=="[yourdevicevendor]", MODE="0664", GROUP="plugdev"

حيث تكون قيمة [yourdevicevendor] هي 2341 إذا كان جهازك Arduino مثلاً. يمكن أيضًا إضافة ATTR{idProduct} لقاعدة أكثر تحديدًا. يجب أن يكون user عضو في مجموعة plugdev. بعد ذلك، ما عليك سوى إعادة توصيل جهازك.

المراجِع

يمكنك إرسال تغريدة إلى @ChromiumDev باستخدام الهاشتاغ #WebUSB وإعلامنا بمكان استخدامك لها وطريقة استخدامك لها.

شكر وتقدير

شكرًا جو ميدلي لمراجعة هذه المقالة.