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

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

François Beaufort
François Beaufort

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

تتطلّب أجهزة USB غير المتوافقة مع المعايير من مورّدي الأجهزة كتابة برامج تشغيل وحِزم تطوير برامج (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 API في المجال، أضفنا هذه الميزة سابقًا في Chrome 54 وChrome 57 باعتبارها مرحلة تجريبية للمصدر.

وانتهت آخر فترة تجريبية بنجاح في أيلول (سبتمبر) 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

حسنًا، لنتعرف الآن على مدى سهولة الاتصال من لوحة 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 يحتوي على 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 على مراجعة هذه المقالة.