เข้าถึงอุปกรณ์ USB บนเว็บ

WebUSB API ช่วยให้ USB ปลอดภัยและใช้งานง่ายขึ้นด้วยการนำไปยังเว็บ

François Beaufort
François Beaufort

ถ้าผมพูดง่ายๆ ว่า "USB" ก็เป็นไปได้ดีที่คุณจะ นึกถึงแป้นพิมพ์ เมาส์ เสียง วิดีโอ และอุปกรณ์จัดเก็บข้อมูล ใช่แล้ว แต่คุณจะพบอุปกรณ์ Universal Serial Bus (USB) ประเภทอื่นรวมอยู่ด้วย

อุปกรณ์ USB ที่ไม่เป็นมาตรฐานเหล่านี้กำหนดให้ผู้ให้บริการฮาร์ดแวร์ต้องเขียนไดรเวอร์และ SDK เฉพาะแพลตฟอร์มเพื่อให้คุณ (นักพัฒนาซอฟต์แวร์) ได้ใช้ประโยชน์ เราต้องขออภัยที่โค้ดเฉพาะแพลตฟอร์มนี้เคยป้องกันไม่ให้เว็บใช้อุปกรณ์เหล่านี้ นี่จึงเป็นเหตุผลหนึ่งที่เราสร้าง WebUSB API ขึ้นมา เพื่อแสดงวิธีการแสดงบริการอุปกรณ์ USB บนเว็บ API นี้ช่วยให้ผู้ผลิตฮาร์ดแวร์สร้าง JavaScript SDK ข้ามแพลตฟอร์มสำหรับอุปกรณ์ของตนเองได้

แต่สิ่งสำคัญที่สุดคือการดำเนินการนี้จะทำให้ USB ปลอดภัยและใช้งานง่ายขึ้นด้วยการนำ 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

พร้อมให้ทดลองใช้จากต้นทาง

ก่อนหน้านี้เราได้เพิ่มฟีเจอร์นี้ใน Chrome 54 และ Chrome 57 เป็นช่วงทดลองใช้จากต้นทางเพื่อรับความคิดเห็นจากนักพัฒนาแอปที่ใช้ WebUSB API ในช่องให้ได้มากที่สุด

การทดลองใช้ล่าสุดได้สิ้นสุดลงแล้วในเดือนกันยายน 2017

ความเป็นส่วนตัวและความปลอดภัย

HTTPS เท่านั้น

เนื่องด้วยความสามารถของฟีเจอร์นี้ ฟีเจอร์นี้จะทำงานได้ในบริบทที่ปลอดภัยเท่านั้น ซึ่งหมายความว่าคุณจะต้องสร้างโดยใช้ TLS

ต้องใช้ท่าทางสัมผัสของผู้ใช้

เพื่อเป็นการป้องกันความปลอดภัย navigator.usb.requestDevice() อาจเรียกใช้ผ่านท่าทางสัมผัสของผู้ใช้ เช่น การแตะหรือการคลิกเมาส์เท่านั้น

นโยบายสิทธิ์

นโยบายสิทธิ์เป็นกลไกที่ช่วยให้นักพัฒนาซอฟต์แวร์เลือกเปิดและปิดใช้ฟีเจอร์และ API ต่างๆ ของเบราว์เซอร์ได้ โดยสามารถระบุผ่านส่วนหัว HTTP และ/หรือแอตทริบิวต์ "อนุญาต" ของ iframe

คุณสามารถกำหนดนโยบายสิทธิ์ที่ควบคุมว่าแอตทริบิวต์ 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 เป็นอย่างมาก หากคุณยังไม่คุ้นเคยกับฟีเจอร์นี้ โปรดดูบทแนะนำ Promises ที่ยอดเยี่ยมนี้ อีกเรื่องหนึ่งคือ () => {} คือฟังก์ชันลูกศรของ ECMAScript 2015

รับสิทธิ์เข้าถึงอุปกรณ์ USB

คุณอาจแจ้งให้ผู้ใช้เลือกอุปกรณ์ USB 1 เครื่องที่เชื่อมต่ออยู่โดยใช้ 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 ID

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 ของหน้า Landing Page ไว้ Chrome จะแสดงการแจ้งเตือนอยู่เรื่อยๆ เมื่อเสียบปลั๊กอุปกรณ์ USB การคลิกการแจ้งเตือนนี้จะเปิดหน้า Landing Page

ภาพหน้าจอของการแจ้งเตือน WebUSB ใน Chrome
การแจ้งเตือน WebUSB

คุยกับกระดาน USB ของ Arduino

คราวนี้มาดูกันว่าการสื่อสารจาก บอร์ด 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 ที่ฉันใช้เพียงใช้โปรโตคอลตัวอย่าง 1 รายการ (ตามโปรโตคอลซีเรียล 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 ของบุคคลที่สามที่ใช้ในโค้ดตัวอย่างด้านบนจะดำเนินการ 2 อย่างดังนี้

  • อุปกรณ์จะทำหน้าที่เป็นอุปกรณ์ WebUSB ที่ช่วยให้ Chrome อ่าน URL ของหน้า Landing Page ได้
  • เพื่อแสดง 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)
  • การโอน ISOCHRONOUS ซึ่งใช้สำหรับสตรีมข้อมูล เช่น วิดีโอและเสียง จะจัดการด้วย isochronousTransferIn(endpointNumber, packetLengths) และ isochronousTransferOut(endpointNumber, data, packetLengths)
  • การโอนแบบกลุ่มซึ่งใช้เพื่อโอนข้อมูลที่ไม่ละเอียดอ่อนเวลาจำนวนมากด้วยวิธีที่เชื่อถือได้จะได้รับการจัดการด้วย transferIn(endpointNumber, length) และ transferOut(endpointNumber, data)

คุณอาจดูโปรเจ็กต์ WebLight ของ Mike Tsao ซึ่งให้ตัวอย่างเบื้องต้นในการสร้างอุปกรณ์ 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 ที่รอดำเนินการ การแบ่งข้อมูลออกเป็นธุรกรรมที่เล็กลงและส่งข้อมูลครั้งละ 2-3 รายการเท่านั้นจะช่วยหลีกเลี่ยงข้อจำกัดเหล่านั้นได้ นอกจากนี้ยังช่วยลดปริมาณหน่วยความจำที่ใช้และทำให้แอปพลิเคชันรายงานความคืบหน้าเมื่อการโอนเสร็จสมบูรณ์ได้อีกด้วย

เนื่องจากการโอนหลายรายการที่ส่งไปยังปลายทางจะดำเนินการตามลำดับเสมอ จึงอาจปรับปรุงอัตราการส่งข้อมูลด้วยการส่งชิ้นส่วนที่เข้าคิวหลายรายการได้เพื่อหลีกเลี่ยงความล่าช้าระหว่างการโอนผ่าน 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 เสมือนได้ วิธีนี้จะเป็นประโยชน์สำหรับการทดสอบ UI โดยไม่ต้องใช้ฮาร์ดแวร์จริง

ภาพหน้าจอของหน้าภายในสำหรับแก้ไขข้อบกพร่องของ 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 ที่อ่านบทความนี้