دسترسی به دستگاه های USB در وب

WebUSB API با آوردن USB به وب، استفاده از آن را ایمن‌تر و آسان‌تر می‌کند.

فرانسوا بوفور
François Beaufort

اگر واضح و ساده بگویم "USB"، احتمال زیادی وجود دارد که بلافاصله به صفحه کلید، ماوس، صدا، ویدئو و دستگاه های ذخیره سازی فکر کنید. حق با شماست، اما انواع دیگری از دستگاه های گذرگاه سریال جهانی (USB) را در آنجا خواهید یافت.

این دستگاه‌های غیراستاندارد USB به فروشندگان سخت‌افزار نیاز دارند که درایورها و SDK‌های مخصوص پلتفرم را بنویسند تا شما (توسعه‌دهنده) از آنها استفاده کنید. متأسفانه این کد مخصوص پلتفرم از لحاظ تاریخی از استفاده این دستگاه ها توسط وب جلوگیری می کند. و این یکی از دلایلی است که WebUSB API ایجاد شده است: ارائه راهی برای نمایش خدمات دستگاه USB در وب. با استفاده از این API، سازندگان سخت‌افزار می‌توانند SDKهای جاوا اسکریپت چند پلتفرمی را برای دستگاه‌های خود بسازند.

اما مهمتر از همه، این امر با آوردن USB به وب، استفاده از آن را ایمن تر و آسان تر می کند .

بیایید رفتاری را که با WebUSB API انتظار دارید ببینیم:

  1. یک دستگاه USB بخرید.
  2. آن را به کامپیوتر خود وصل کنید. یک اعلان بلافاصله ظاهر می شود، با وب سایت مناسب برای رفتن به آن برای این دستگاه.
  3. روی اعلان کلیک کنید. وب سایت وجود دارد و آماده استفاده است!
  4. برای اتصال کلیک کنید و یک انتخابگر دستگاه USB در Chrome ظاهر می شود که می توانید دستگاه خود را انتخاب کنید.

تادا!

این رویه بدون WebUSB API چگونه خواهد بود؟

  1. یک اپلیکیشن مخصوص پلتفرم را نصب کنید.
  2. اگر حتی در سیستم عامل من پشتیبانی می شود، بررسی کنید که فایل را درست دانلود کرده ام.
  3. چیز را نصب کنید. اگر خوش شانس باشید، هیچ پیام ترسناک سیستم عامل یا پنجره بازشوی دریافت نمی کنید که در مورد نصب درایورها/برنامه های کاربردی از اینترنت به شما هشدار دهد. اگر بدشانس باشید، درایورها یا برنامه های نصب شده دچار مشکل شده و به رایانه شما آسیب می رساند. (به یاد داشته باشید، وب به گونه ای ساخته شده است که دارای وب سایت های نادرست باشد ).
  4. اگر فقط یک بار از این ویژگی استفاده کنید، کد روی رایانه شما باقی می ماند تا زمانی که فکر کنید آن را حذف کنید. (در وب، فضای استفاده نشده در نهایت بازیابی می شود.)

قبل از شروع

این مقاله فرض می‌کند که شما اطلاعات اولیه در مورد نحوه عملکرد USB دارید. اگر نه، توصیه می‌کنم USB را در NutShell بخوانید. برای اطلاعات پس زمینه درباره USB، مشخصات رسمی USB را بررسی کنید.

WebUSB API در Chrome 61 موجود است.

برای آزمایشات منشا موجود است

به منظور دریافت هرچه بیشتر بازخورد از برنامه‌نویسانی که از WebUSB API در این زمینه استفاده می‌کنند، قبلاً این ویژگی را در Chrome 54 و Chrome 57 به عنوان نسخه آزمایشی اصلی اضافه کرده‌ایم.

آخرین آزمایش در سپتامبر 2017 با موفقیت به پایان رسید.

حریم خصوصی و امنیت

فقط HTTPS

به دلیل قدرت این ویژگی، فقط در زمینه های امن کار می کند. این بدان معناست که شما باید با در نظر گرفتن TLS بسازید.

اشاره کاربر مورد نیاز است

به عنوان یک اقدام احتیاطی امنیتی، navigator.usb.requestDevice() فقط از طریق یک حرکت کاربر مانند لمس یا کلیک ماوس فراخوانی می شود.

خط مشی مجوزها

خط مشی مجوزها مکانیزمی است که به توسعه دهندگان اجازه می دهد تا به طور انتخابی ویژگی های مرورگر و API های مختلف را فعال و غیرفعال کنند. می توان آن را از طریق یک هدر HTTP و/یا یک ویژگی iframe "allow" تعریف کرد.

شما می‌توانید یک خط‌مشی مجوزها تعریف کنید که کنترل می‌کند آیا ویژگی usb روی شی ناوبر قرار می‌گیرد یا به عبارت دیگر اگر به WebUSB اجازه دهید.

در زیر نمونه ای از یک خط مشی هدر آمده است که در آن WebUSB مجاز نیست:

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

در زیر نمونه دیگری از یک خط مشی کانتینر وجود دارد که در آن USB مجاز است:

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

بیایید شروع به کدنویسی کنیم

WebUSB API به شدت به وعده های جاوا اسکریپت متکی است. اگر با آنها آشنایی ندارید، این آموزش عالی Promises را بررسی کنید. یک چیز دیگر، () => {} به سادگی توابع پیکان ECMAScript 2015 هستند.

به دستگاه های USB دسترسی پیدا کنید

می‌توانید از کاربر بخواهید که یک دستگاه USB متصل را با استفاده از navigator.usb.requestDevice() انتخاب کند یا با navigator.usb.getDevices() تماس بگیرد تا فهرستی از تمام دستگاه‌های USB متصل شده به وب‌سایت به آنها دسترسی داشته باشد.

تابع navigator.usb.requestDevice() یک شی جاوا اسکریپت اجباری می گیرد که filters تعریف می کند. این فیلترها برای تطبیق هر دستگاه USB با شناسه فروشنده داده شده ( vendorId ) و به صورت اختیاری، محصول ( productId ) استفاده می شود. کلیدهای classCode ، protocolCode ، serialNumber و subclassCode نیز می‌توانند در آنجا تعریف شوند.

عکس صفحه درخواست کاربر دستگاه USB در کروم
درخواست کاربر دستگاه USB.

به عنوان مثال، در اینجا نحوه دسترسی به یک دستگاه آردوینو متصل پیکربندی شده برای اجازه دادن به مبدا وجود دارد.

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 جستجو کردم.

device USB که در وعده انجام شده در بالا بازگردانده شده است دارای برخی اطلاعات اساسی و در عین حال مهم در مورد دستگاه مانند نسخه 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 در کروم
اطلاع رسانی WebUSB.

با یک برد USB آردوینو صحبت کنید

خوب، حالا بیایید ببینیم که ارتباط از طریق یک برد آردوینو سازگار با 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) را پیاده‌سازی می‌کند و تولیدکنندگان می‌توانند هر مجموعه و انواع نقطه‌های پایانی را که می‌خواهند ایجاد کنند. انتقال‌های کنترلی مخصوصاً برای دستورات پیکربندی کوچک خوب هستند، زیرا اولویت اتوبوس را دارند و ساختاری کاملاً مشخص دارند.

و در اینجا طرحی است که در برد آردوینو آپلود شده است.

// 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 صفحه فرود را بخواند.
  • این یک API سریال WebUSB را نشان می دهد که می توانید از آن برای لغو پیش فرض استفاده کنید.

دوباره به کد جاوا اسکریپت نگاه کنید. پس از انتخاب 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) انجام می‌شود.
  • انتقال‌های INTERRUPT که برای مقدار کمی از داده‌های حساس به زمان استفاده می‌شود، با همان روش‌هایی که انتقال‌های انبوه با transferIn(endpointNumber, length) و transferOut(endpointNumber, data) انجام می‌شود.
  • انتقال‌های ISOCHRONOUS، که برای جریان‌های داده‌ای مانند ویدیو و صدا استفاده می‌شود، با هم‌زمان isochronousTransferIn(endpointNumber, packetLengths) و isochronousTransferOut(endpointNumber, data, packetLengths) انجام می‌شود.
  • انتقال انبوه، که برای انتقال حجم زیادی از داده های غیر حساس به زمان به روشی قابل اعتماد استفاده می شود، با transferIn(endpointNumber, length) و transferOut(endpointNumber, data) انجام می شود.

همچنین ممکن است بخواهید به پروژه WebLight مایک تسائو نگاهی بیندازید که یک نمونه اولیه از ساخت یک دستگاه LED کنترل شده با USB را ارائه می دهد که برای WebUSB API طراحی شده است (در اینجا از آردوینو استفاده نمی شود). سخت افزار، نرم افزار و سیستم عامل را خواهید یافت.

دسترسی به یک دستگاه 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 در کروم
صفحه ورود دستگاه در کروم برای رفع اشکال WebUSB API.

صفحه داخلی about://usb-internals نیز مفید است و به شما امکان می دهد اتصال و قطع اتصال دستگاه های مجازی WebUSB را شبیه سازی کنید. این برای انجام تست UI بدون سخت افزار واقعی مفید است.

نماگرفت صفحه داخلی برای اشکال زدایی WebUSB در کروم
صفحه داخلی در کروم برای رفع اشکال WebUSB API.

در اکثر سیستم های لینوکس، دستگاه های USB به طور پیش فرض با مجوزهای فقط خواندنی نقشه برداری می شوند. برای اینکه به Chrome اجازه دهید یک دستگاه USB را باز کند، باید یک قانون udev جدید اضافه کنید. یک فایل در /etc/udev/rules.d/50-yourdevicename.rules با محتوای زیر ایجاد کنید:

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

برای مثال اگر دستگاه شما آردوینو باشد، [yourdevicevendor] 2341 است. ATTR{idProduct} همچنین می‌تواند برای یک قانون خاص‌تر اضافه شود. مطمئن شوید که user شما عضو گروه plugdev است. سپس، فقط دستگاه خود را دوباره وصل کنید.

منابع

با استفاده از هشتگ #WebUSB یک توییت به ChromiumDev@ ارسال کنید و به ما اطلاع دهید کجا و چگونه از آن استفاده می‌کنید.

سپاسگزاریها

با تشکر از جو مدلی برای بررسی این مقاله.