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

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

François Beaufort
François Beaufort

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

อุปกรณ์ USB ที่ไม่เป็นไปตามมาตรฐานเหล่านี้กำหนดให้ผู้ให้บริการฮาร์ดแวร์ต้องเขียนไดรเวอร์และ SDK สำหรับแพลตฟอร์มที่เฉพาะเจาะจงเพื่อให้คุณ (นักพัฒนาแอป) ใช้ประโยชน์จากอุปกรณ์ได้ แต่ที่ผ่านมารหัสเฉพาะแพลตฟอร์มนี้ทำให้เว็บไม่สามารถใช้อุปกรณ์เหล่านี้ได้ ด้วยเหตุนี้ WebUSB API จึงถือกำเนิดขึ้นเพื่อใช้เป็นช่องทางในการแสดงบริการของอุปกรณ์ USB บนเว็บ API นี้จะช่วยให้ผู้ผลิตฮาร์ดแวร์สามารถสร้าง SDK ของ 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 พร้อมใช้งานใน Chrome 61

พร้อมใช้งานสำหรับช่วงทดลองใช้จากต้นทาง

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

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

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

HTTPS เท่านั้น

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

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

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

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

นโยบายสิทธิ์เป็นกลไกที่ช่วยให้นักพัฒนาแอปเปิดใช้และปิดใช้ฟีเจอร์และ API ต่างๆ ของเบราว์เซอร์ได้ ซึ่งสามารถกําหนดผ่านส่วนหัว HTTP และ/หรือแอตทริบิวต์ "allow" ของ 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 อาศัย Promises ของ JavaScript เป็นอย่างมาก หากไม่คุ้นเคยกับ Promise ให้ดูบทแนะนำ Promise ที่ยอดเยี่ยมนี้ อีกอย่าง () => {} คือ ฟังก์ชันลูกศรของ 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); });

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

ภาพหน้าจอของการแจ้งเตือน 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 ของหน้า 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 ทั้งหมดต่อไปนี้

  • การโอน CONTROL ซึ่งใช้ส่งหรือรับพารามิเตอร์การกําหนดค่าหรือคําสั่งไปยังอุปกรณ์ 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 rule ใหม่ สร้างไฟล์ที่ /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 ที่ตรวจสอบบทความนี้