การเชื่อมต่อกับอุปกรณ์ HID ที่ไม่ปกติ

WebHID API อนุญาตให้เว็บไซต์เข้าถึงคีย์บอร์ดเสริมและเกมแพดที่แปลกใหม่

François Beaufort
François Beaufort

มีอุปกรณ์ที่โต้ตอบกับมนุษย์โดยตรง (HID) จำนวนมาก เช่น แป้นพิมพ์ทางเลือกหรือเกมแพดที่แปลกใหม่ ซึ่งใหม่เกินไป เก่าเกินไป หรือไม่ค่อยพบเห็น ทำให้ไดรเวอร์อุปกรณ์ของระบบเข้าถึงไม่ได้ WebHID API แก้ปัญหานี้ด้วยการ ให้วิธีใช้ตรรกะเฉพาะอุปกรณ์ใน JavaScript

กรณีการใช้งานที่แนะนำ

อุปกรณ์ HID รับอินพุตจากหรือแสดงเอาต์พุตต่อมนุษย์ ตัวอย่างอุปกรณ์ ได้แก่ แป้นพิมพ์ อุปกรณ์ชี้ (เมาส์ หน้าจอสัมผัส ฯลฯ) และเกมแพด โปรโตคอล HID ช่วยให้เข้าถึงอุปกรณ์เหล่านี้ในคอมพิวเตอร์เดสก์ท็อป ได้โดยใช้ไดรเวอร์ระบบปฏิบัติการ แพลตฟอร์มเว็บรองรับอุปกรณ์ HID โดยใช้ไดรเวอร์เหล่านี้

การเข้าถึงอุปกรณ์ HID ที่ไม่ใช่แบบที่ใช้กันทั่วไปไม่ได้เป็นปัญหาที่น่ากังวลอย่างยิ่งเมื่อพูดถึงแป้นพิมพ์เสริมทางเลือก (เช่น Elgato Stream Deck, ชุดหูฟัง Jabra, X-keys) และการรองรับเกมแพดที่แปลกใหม่ เกมแพดที่ออกแบบมาสำหรับเดสก์ท็อป มักใช้ HID สำหรับอินพุตเกมแพด (ปุ่ม จอยสติ๊ก ทริกเกอร์) และเอาต์พุต (LED การสั่น) อย่างไรก็ตาม อินพุตและเอาต์พุตของเกมแพดไม่ได้ เป็นมาตรฐานที่ดี และเว็บเบราว์เซอร์มักต้องใช้ตรรกะที่กำหนดเองสำหรับอุปกรณ์บางอย่าง ซึ่งเป็นสิ่งที่ยั่งยืนไม่ได้และส่งผลให้การสนับสนุนอุปกรณ์รุ่นเก่าและ อุปกรณ์ที่ไม่ค่อยพบนั้นไม่ดี นอกจากนี้ยังทำให้เบราว์เซอร์ต้องพึ่งพาข้อบกพร่องในลักษณะการทำงาน ของอุปกรณ์บางรุ่นด้วย

คำศัพท์

HID ประกอบด้วยแนวคิดพื้นฐาน 2 อย่าง ได้แก่ รายงานและตัวอธิบายรายงาน รายงานคือข้อมูลที่แลกเปลี่ยนระหว่างอุปกรณ์และไคลเอ็นต์ซอฟต์แวร์ ตัวอธิบายรายงานจะอธิบายรูปแบบและความหมายของข้อมูลที่อุปกรณ์รองรับ

HID (Human Interface Device) คืออุปกรณ์ประเภทหนึ่งที่รับอินพุตจากหรือ แสดงเอาต์พุตต่อมนุษย์ นอกจากนี้ยังหมายถึงโปรโตคอล HID ซึ่งเป็นมาตรฐานสำหรับการสื่อสารแบบ 2 ทางระหว่างโฮสต์กับอุปกรณ์ที่ออกแบบมาเพื่อลดความซับซ้อนของขั้นตอนการติดตั้ง เดิมทีโปรโตคอล HID ได้รับการพัฒนาขึ้น สำหรับอุปกรณ์ USB แต่ต่อมาได้มีการนำไปใช้กับโปรโตคอลอื่นๆ อีกมากมาย รวมถึงบลูทูธ

แอปพลิเคชันและอุปกรณ์ HID แลกเปลี่ยนข้อมูลไบนารีผ่านรายงาน 3 ประเภท ได้แก่

ประเภทรายงาน คำอธิบาย
รายงานอินพุต ข้อมูลที่ส่งจากอุปกรณ์ไปยังแอปพลิเคชัน (เช่น มีการกดปุ่ม)
รายงานเอาต์พุต ข้อมูลที่ส่งจากแอปพลิเคชันไปยังอุปกรณ์ (เช่น คำขอเปิดไฟแบ็กไลท์ของคีย์บอร์ด)
รายงานฟีเจอร์ ข้อมูลที่อาจส่งได้ทั้ง 2 ทาง รูปแบบนี้จะขึ้นอยู่กับอุปกรณ์

ตัวอธิบายรายงานจะอธิบายรูปแบบไบนารีของรายงานที่อุปกรณ์รองรับ โครงสร้างของรายงานเป็นแบบลำดับชั้นและสามารถจัดกลุ่มรายงานไว้ด้วยกันเป็นคอลเล็กชันที่แตกต่างกัน ภายในคอลเล็กชันระดับบนสุด รูปแบบของตัวอธิบายกำหนดโดยข้อกำหนด HID

การใช้งาน HID คือค่าตัวเลขที่อ้างอิงถึงอินพุตหรือเอาต์พุตมาตรฐาน ค่าการใช้งานช่วยให้อุปกรณ์อธิบายการใช้งานที่ตั้งใจไว้ของอุปกรณ์และ วัตถุประสงค์ของแต่ละฟิลด์ในรายงานได้ เช่น กำหนดไว้สำหรับปุ่มซ้ายของเมาส์ นอกจากนี้ ระบบยังจัดระเบียบการใช้งานเป็นหน้าการใช้งาน ซึ่งจะระบุหมวดหมู่ระดับสูงของอุปกรณ์หรือรายงาน

การใช้ WebHID API

การตรวจหาฟีเจอร์

หากต้องการตรวจสอบว่าระบบรองรับ WebHID API หรือไม่ ให้ใช้

if ("hid" in navigator) {
  // The WebHID API is supported.
}

เปิดการเชื่อมต่อ HID

WebHID API ได้รับการออกแบบมาให้ทำงานแบบไม่พร้อมกันเพื่อป้องกันไม่ให้ UI ของเว็บไซต์ถูกบล็อกเมื่อรอรับอินพุต ซึ่งถือเป็นสิ่งสำคัญเนื่องจากสามารถรับข้อมูล HID ได้ทุกเมื่อ จึงต้องมีวิธีรับฟังข้อมูลดังกล่าว

หากต้องการเปิดการเชื่อมต่อ HID ให้เข้าถึงออบเจ็กต์ HIDDevice ก่อน โดยคุณสามารถทำได้โดย แจ้งให้ผู้ใช้เลือกอุปกรณ์โดยเรียกใช้ navigator.hid.requestDevice() หรือเลือกอุปกรณ์จาก navigator.hid.getDevices() ซึ่งจะแสดงรายการอุปกรณ์ที่เว็บไซต์ได้รับสิทธิ์เข้าถึง ก่อนหน้านี้

navigator.hid.requestDevice() ฟังก์ชันนี้ใช้ออบเจ็กต์ที่จำเป็นซึ่ง กำหนดตัวกรอง ซึ่งใช้เพื่อจับคู่อุปกรณ์ที่เชื่อมต่อกับตัวระบุผู้ให้บริการ USB (vendorId), ตัวระบุผลิตภัณฑ์ USB (productId), ค่าหน้าการใช้งาน (usagePage) และค่าการใช้งาน (usage) คุณสามารถดูค่าดังกล่าวได้จากที่เก็บรหัส USB และเอกสารตารางการใช้งาน HID

ออบเจ็กต์ HIDDevice หลายรายการที่ฟังก์ชันนี้แสดงผลแสดงถึงอินเทอร์เฟซ HID หลายรายการในอุปกรณ์จริงเดียวกัน

// Filter on devices with the Nintendo Switch Joy-Con USB Vendor/Product IDs.
const filters = [
  {
    vendorId: 0x057e, // Nintendo Co., Ltd
    productId: 0x2006 // Joy-Con Left
  },
  {
    vendorId: 0x057e, // Nintendo Co., Ltd
    productId: 0x2007 // Joy-Con Right
  }
];

// Prompt user to select a Joy-Con device.
const [device] = await navigator.hid.requestDevice({ filters });
// Get all devices the user has previously granted the website access to.
const devices = await navigator.hid.getDevices();
ภาพหน้าจอของข้อความแจ้งอุปกรณ์ HID บนเว็บไซต์
พรอมต์ของผู้ใช้สำหรับการเลือก Nintendo Switch Joy-Con

นอกจากนี้ คุณยังใช้คีย์ exclusionFilters ที่ไม่บังคับใน navigator.hid.requestDevice() เพื่อยกเว้นอุปกรณ์บางอย่างจากตัวเลือกเบราว์เซอร์ได้ ซึ่งเป็นอุปกรณ์ที่ทราบว่าทำงานผิดปกติ เป็นต้น

// Request access to a device with vendor ID 0xABCD. The device must also have
// a collection with usage page Consumer (0x000C) and usage ID Consumer
// Control (0x0001). The device with product ID 0x1234 is malfunctioning.
const [device] = await navigator.hid.requestDevice({
  filters: [{ vendorId: 0xabcd, usagePage: 0x000c, usage: 0x0001 }],
  exclusionFilters: [{ vendorId: 0xabcd, productId: 0x1234 }],
});

ออบเจ็กต์ HIDDevice มีตัวระบุผู้ให้บริการและผลิตภัณฑ์ USB สำหรับการระบุอุปกรณ์ collections ของอุปกรณ์จะเริ่มต้นด้วยคำอธิบายแบบลำดับชั้น ของรูปแบบรายงานของอุปกรณ์

for (let collection of device.collections) {
  // An HID collection includes usage, usage page, reports, and subcollections.
  console.log(`Usage: ${collection.usage}`);
  console.log(`Usage page: ${collection.usagePage}`);

  for (let inputReport of collection.inputReports) {
    console.log(`Input report: ${inputReport.reportId}`);
    // Loop through inputReport.items
  }

  for (let outputReport of collection.outputReports) {
    console.log(`Output report: ${outputReport.reportId}`);
    // Loop through outputReport.items
  }

  for (let featureReport of collection.featureReports) {
    console.log(`Feature report: ${featureReport.reportId}`);
    // Loop through featureReport.items
  }

  // Loop through subcollections with collection.children
}

โดยค่าเริ่มต้นแล้ว อุปกรณ์ HIDDevice จะอยู่ในสถานะ "ปิด" และต้องเปิดโดยการโทรหา open() ก่อนจึงจะส่งหรือรับข้อมูลได้

// Wait for the HID connection to open before sending/receiving data.
await device.open();

รับรายงานอินพุต

เมื่อสร้างการเชื่อมต่อ HID แล้ว คุณจะจัดการรายงานอินพุตที่เข้ามาได้โดยการฟังเหตุการณ์ "inputreport" จากอุปกรณ์ เหตุการณ์เหล่านั้น มีข้อมูล HID เป็นออบเจ็กต์ DataView (data), อุปกรณ์ HID ที่เป็นของเหตุการณ์ (device) และรหัสรายงาน 8 บิตที่เชื่อมโยงกับรายงานอินพุต (reportId)

รูปภาพ Nintendo Switch สีแดงและสีน้ำเงิน
อุปกรณ์ Nintendo Switch Joy-Con

จากตัวอย่างก่อนหน้า โค้ดด้านล่างจะแสดงวิธีตรวจหาปุ่มที่ผู้ใช้กดบนอุปกรณ์ Joy-Con Right เพื่อให้คุณลองใช้ที่บ้านได้

device.addEventListener("inputreport", event => {
  const { data, device, reportId } = event;

  // Handle only the Joy-Con Right device and a specific report ID.
  if (device.productId !== 0x2007 && reportId !== 0x3f) return;

  const value = data.getUint8(0);
  if (value === 0) return;

  const someButtons = { 1: "A", 2: "X", 4: "B", 8: "Y" };
  console.log(`User pressed button ${someButtons[value]}.`);
});

ดูเดโม webhid-joycon-button ของปากกา

ส่งรายงานเอาต์พุต

หากต้องการส่งรายงานเอาต์พุตไปยังอุปกรณ์ HID ให้ส่งรหัสรายงาน 8 บิตที่เชื่อมโยง กับรายงานเอาต์พุต (reportId) และไบต์เป็น BufferSource (data) ไปยัง device.sendReport() Promise ที่ส่งคืนจะได้รับการแก้ไขเมื่อระบบส่งรายงานแล้ว หากอุปกรณ์ HID ไม่ใช้รหัสรายงาน ให้ตั้งค่า reportId เป็น 0

ตัวอย่างด้านล่างใช้กับอุปกรณ์ Joy-Con และแสดงวิธีทำให้ สั่นด้วยรายงานเอาต์พุต

// First, send a command to enable vibration.
// Magical bytes come from https://github.com/mzyy94/joycon-toolweb
const enableVibrationData = [1, 0, 1, 64, 64, 0, 1, 64, 64, 0x48, 0x01];
await device.sendReport(0x01, new Uint8Array(enableVibrationData));

// Then, send a command to make the Joy-Con device rumble.
// Actual bytes are available in the sample below.
const rumbleData = [ /* ... */ ];
await device.sendReport(0x10, new Uint8Array(rumbleData));

ดูเดโม webhid-joycon-rumble ของ Pen

ส่งและรับรายงานฟีเจอร์

รายงานฟีเจอร์เป็นรายงานข้อมูล HID ประเภทเดียวที่สามารถเดินทางได้ทั้ง 2 ทิศทาง ซึ่งช่วยให้อุปกรณ์และแอปพลิเคชัน HID แลกเปลี่ยนข้อมูล HID ที่ไม่ได้มาตรฐานได้ รายงานฟีเจอร์ไม่เหมือนกับรายงานอินพุตและเอาต์พุตตรงที่แอปพลิเคชันจะไม่ได้รับหรือส่งรายงานเป็นประจำ

ภาพคอมพิวเตอร์แล็ปท็อปสีดำและสีเงิน
แป้นพิมพ์แล็ปท็อป

หากต้องการส่งรายงานฟีเจอร์ไปยังอุปกรณ์ HID ให้ส่งรหัสรายงาน 8 บิตที่เชื่อมโยง กับรายงานฟีเจอร์ (reportId) และไบต์เป็น BufferSource (data) ไปยัง device.sendFeatureReport() Promise ที่ส่งคืนจะได้รับการแก้ไขเมื่อส่งรายงานแล้ว หากอุปกรณ์ HID ไม่ใช้รหัสรายงาน ให้ตั้งค่า reportId เป็น 0

ตัวอย่างด้านล่างแสดงการใช้รายงานฟีเจอร์โดยแสดงวิธี ขออุปกรณ์ไฟแบ็กไลท์ของคีย์บอร์ด Apple เปิดอุปกรณ์ และทำให้อุปกรณ์กะพริบ

const waitFor = duration => new Promise(r => setTimeout(r, duration));

// Prompt user to select an Apple Keyboard Backlight device.
const [device] = await navigator.hid.requestDevice({
  filters: [{ vendorId: 0x05ac, usage: 0x0f, usagePage: 0xff00 }]
});

// Wait for the HID connection to open.
await device.open();

// Blink!
const reportId = 1;
for (let i = 0; i < 10; i++) {
  // Turn off
  await device.sendFeatureReport(reportId, Uint32Array.from([0, 0]));
  await waitFor(100);
  // Turn on
  await device.sendFeatureReport(reportId, Uint32Array.from([512, 0]));
  await waitFor(100);
}

ดูเดโม webhid-apple-keyboard-backlight ของปากกา

หากต้องการรับรายงานฟีเจอร์จากอุปกรณ์ HID ให้ส่งรหัสรายงาน 8 บิต ที่เชื่อมโยงกับรายงานฟีเจอร์ (reportId) ไปยัง device.receiveFeatureReport() Promise ที่ส่งคืนจะได้รับการแก้ไขด้วยออบเจ็กต์ DataView ที่มีเนื้อหาของรายงานฟีเจอร์ หากอุปกรณ์ HID ไม่ได้ใช้รหัสรายงาน ให้ตั้งค่า reportId เป็น 0

// Request feature report.
const dataView = await device.receiveFeatureReport(/* reportId= */ 1);

// Read feature report contents with dataView.getInt8(), getUint8(), etc...

ฟังการเชื่อมต่อและการยกเลิกการเชื่อมต่อ

เมื่อได้รับสิทธิ์เข้าถึงอุปกรณ์ HID แล้ว เว็บไซต์จะ รับเหตุการณ์การเชื่อมต่อและการยกเลิกการเชื่อมต่อได้อย่างต่อเนื่องโดยการฟังเหตุการณ์ "connect" และ "disconnect"

navigator.hid.addEventListener("connect", event => {
  // Automatically open event.device or warn user a device is available.
});

navigator.hid.addEventListener("disconnect", event => {
  // Remove |event.device| from the UI.
});

เพิกถอนสิทธิ์เข้าถึงอุปกรณ์ HID

เว็บไซต์สามารถล้างสิทธิ์เข้าถึงอุปกรณ์ HID ที่ไม่ต้องการเก็บรักษาอีกต่อไปได้โดยการเรียกใช้ forget() ในอินสแตนซ์ HIDDevice ตัวอย่างเช่น สำหรับเว็บแอปพลิเคชันเพื่อการศึกษาที่ใช้ในคอมพิวเตอร์ที่ใช้ร่วมกันกับอุปกรณ์จำนวนมาก สิทธิ์ที่ผู้ใช้สร้างขึ้นจำนวนมากที่สะสมไว้จะทำให้ประสบการณ์ของผู้ใช้ไม่ดี

การเรียกใช้ forget() ในอินสแตนซ์ HIDDevice เดียวจะเป็นการเพิกถอนสิทธิ์เข้าถึงอินเทอร์เฟซ HID ทั้งหมดในอุปกรณ์จริงเดียวกัน

// Voluntarily revoke access to this HID device.
await device.forget();

เนื่องจาก forget() พร้อมใช้งานใน Chrome 100 ขึ้นไป ให้ตรวจสอบว่าฟีเจอร์นี้ รองรับรายการต่อไปนี้หรือไม่

if ("hid" in navigator && "forget" in HIDDevice.prototype) {
  // forget() is supported.
}

เคล็ดลับสำหรับนักพัฒนาแอป

การแก้ไขข้อบกพร่องของ HID ใน Chrome เป็นเรื่องง่ายด้วยหน้าภายใน about://device-log ซึ่งคุณสามารถดูเหตุการณ์ทั้งหมดที่เกี่ยวข้องกับอุปกรณ์ HID และ USB ได้ในที่เดียว

ภาพหน้าจอของหน้าภายในเพื่อแก้ไขข้อบกพร่องของ HID
หน้าภายในใน Chrome สำหรับการแก้ไขข้อบกพร่องของ HID

ดูเครื่องมือสำรวจ HID เพื่อส่งข้อมูลอุปกรณ์ HID ไปยังรูปแบบที่มนุษย์อ่านได้ โดยจะแมปจากค่าการใช้งานไปยังชื่อสำหรับการใช้งาน HID แต่ละรายการ

ในระบบ Linux ส่วนใหญ่ อุปกรณ์ HID จะได้รับการแมปด้วยสิทธิ์แบบอ่านอย่างเดียวโดยค่าเริ่มต้น หากต้องการอนุญาตให้ Chrome เปิดอุปกรณ์ HID คุณจะต้องเพิ่มกฎ udev ใหม่ สร้างไฟล์ที่ /etc/udev/rules.d/50-yourdevicename.rules โดยมีเนื้อหาต่อไปนี้

KERNEL=="hidraw*", ATTRS{idVendor}=="[yourdevicevendor]", MODE="0664", GROUP="plugdev"

ในบรรทัดด้านบน [yourdevicevendor] คือ 057e หากอุปกรณ์ของคุณเป็น Nintendo Switch Joy-Con เป็นต้น ATTRS{idProduct} ยังเพิ่มเพื่อกฎที่เจาะจงมากขึ้นได้ด้วย ตรวจสอบว่า user เป็นสมาชิกของกลุ่ม plugdev จากนั้นเพียง เชื่อมต่ออุปกรณ์อีกครั้ง

การสนับสนุนเบราว์เซอร์

WebHID API พร้อมใช้งานในแพลตฟอร์มเดสก์ท็อปทั้งหมด (ChromeOS, Linux, macOS และ Windows) ใน Chrome 89

การสาธิต

ตัวอย่าง WebHID บางส่วนแสดงอยู่ที่ web.dev/hid-examples ลองดูเลย

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

ผู้เขียนข้อกำหนดได้ออกแบบและใช้ WebHID API โดยใช้หลักการหลักที่กำหนดไว้ในการควบคุมการเข้าถึงฟีเจอร์ที่มีประสิทธิภาพของแพลตฟอร์มเว็บ ซึ่งรวมถึงการควบคุมของผู้ใช้ ความโปร่งใส และการยศาสตร์ ความสามารถในการใช้ API นี้ส่วนใหญ่จะจำกัดด้วยโมเดลสิทธิ์ที่ให้สิทธิ์เข้าถึงอุปกรณ์ HID เพียงเครื่องเดียวในแต่ละครั้ง เพื่อตอบสนองต่อพรอมต์ของผู้ใช้ ผู้ใช้ต้องดำเนินการ อย่างกระตือรือร้นเพื่อเลือกอุปกรณ์ HID ที่ต้องการ

หากต้องการทำความเข้าใจข้อแลกเปลี่ยนด้านความปลอดภัย โปรดดูส่วนข้อควรพิจารณาด้านความปลอดภัยและความเป็นส่วนตัวของข้อกำหนด WebHID

นอกจากนี้ Chrome ยังตรวจสอบการใช้งานคอลเล็กชันระดับบนสุดแต่ละรายการ และหากคอลเล็กชันระดับบนสุดมีการใช้งานที่ได้รับการปกป้อง (เช่น คีย์บอร์ด เมาส์ทั่วไป) เว็บไซต์จะส่งและรับรายงานใดๆ ที่กำหนดไว้ในคอลเล็กชันนั้นไม่ได้ รายการการใช้งานที่ได้รับการคุ้มครองทั้งหมดพร้อมให้บริการแก่สาธารณะ

โปรดทราบว่าอุปกรณ์ HID ที่มีความละเอียดอ่อนด้านความปลอดภัย (เช่น อุปกรณ์ FIDO HID ที่ใช้สำหรับการ ตรวจสอบสิทธิ์ที่รัดกุมยิ่งขึ้น) จะถูกบล็อกใน Chrome ด้วย ดูไฟล์รายการที่บล็อก USB และ รายการที่บล็อก HID

ความคิดเห็น

ทีม Chrome อยากทราบความคิดเห็นและประสบการณ์ของคุณเกี่ยวกับ WebHID API

บอกเราเกี่ยวกับการออกแบบ API

มีอะไรเกี่ยวกับ API ที่ไม่ทำงานตามที่คาดไว้ไหม หรือมีเมธอดหรือพร็อพเพอร์ตี้ที่ขาดหายไปซึ่งคุณต้องใช้เพื่อนำไอเดียไปใช้ไหม

แจ้งปัญหาเกี่ยวกับข้อกำหนดในที่เก็บ GitHub ของ WebHID API หรือแสดงความคิดเห็น ในปัญหาที่มีอยู่

รายงานปัญหาเกี่ยวกับการติดตั้งใช้งาน

หากพบข้อบกพร่องในการใช้งาน Chrome หรือการติดตั้งใช้งาน แตกต่างจากข้อกำหนด

ดูวิธีรายงานข้อบกพร่องของ WebHID โปรดใส่รายละเอียดให้มากที่สุดเท่าที่จะเป็นไปได้ ระบุวิธีการง่ายๆ ในการจำลองข้อบกพร่อง และตั้งค่าคอมโพเนนต์เป็น Blink>HID

แสดงการสนับสนุน

คุณวางแผนที่จะใช้ WebHID API ใช่ไหม การสนับสนุนแบบสาธารณะของคุณจะช่วยให้ทีม Chrome จัดลําดับความสําคัญของฟีเจอร์และแสดงให้ผู้ให้บริการเบราว์เซอร์รายอื่นๆ เห็นว่าการรองรับฟีเจอร์เหล่านี้มีความสําคัญเพียงใด

ส่งทวีตถึง @ChromiumDev โดยใช้แฮชแท็ก #WebHID และแจ้งให้เราทราบ ว่าคุณใช้ฟีเจอร์นี้ที่ไหนและอย่างไร

ลิงก์ที่มีประโยชน์

การรับทราบ

ขอขอบคุณ Matt Reynolds และ Joe Medley ที่รีวิวบทความนี้ ภาพ Nintendo Switch สีแดงและสีน้ำเงินโดย Sara Kurfeß และภาพคอมพิวเตอร์แล็ปท็อปสีดำและสีเงินโดย Athul Cyriac Ajay ใน Unsplash