เข้าถึงอุปกรณ์ 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 ID นี้

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 ที่ใช้ร่วมกับ WebUSB ได้นั้นทำได้ง่ายเพียงใด ดูวิธีการที่ 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 ทุกประเภทได้ดังนี้

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

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