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

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

François Beaufort
François Beaufort

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

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

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

لنلقِ نظرة على السلوك المتوقّع لواجهة برمجة التطبيقات WebUSB API:

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

بهذه السهولة.

كيف سيكون هذا الإجراء بدون WebUSB API؟

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

قبل البدء

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

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

متوفّر لعمليات التجربة والتقييم

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

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

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

HTTPS فقط

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

يجب أن يُجري المستخدم إيماءة

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

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

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

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

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

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

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

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

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

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

الوصول إلى أجهزة 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

حسنًا، لنلقِ نظرة الآن على مدى سهولة التواصل من لوحة Arduino متوافقة مع 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 يحتوي على DataView dataيجب تحليله بشكلٍ مناسب.

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

أريد المزيد

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

  • يتم التعامل مع عمليات نقل التحكم، التي تُستخدَم لإرسال أو تلقّي مَعلمات الإعدادات أو الأوامر إلى جهاز USB، باستخدام controlTransferIn(setup, length) وcontrolTransferOut(setup, data).
  • إنّ عمليات نقل البيانات المتقطّعة، التي تُستخدَم لنقل كمية صغيرة من البيانات الحسّاسة للوقت، تتم معالجتها باستخدام الطرق نفسها التي تُستخدَم لنقل البيانات المجمّعة باستخدام transferIn(endpointNumber, length) وtransferOut(endpointNumber, data).
  • يتم نقل البيانات بشكل متزامن، ويتم استخدامه لمصادر البيانات مثل الفيديو والصوت، ويتم التعامل معه باستخدام 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() متاح في الإصدار 101 من Chrome أو الإصدارات الأحدث، تحقّق مما إذا كانت هذه الميزة متوفرة مع ما يلي:

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 وأطلِعنا على مكان استخدامك للميزة وطريقة استخدامك لها.

الشكر والتقدير

نشكر Joe Medley على مراجعة هذه المقالة.