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

WebHID API ช่วยให้เว็บไซต์เข้าถึงแป้นพิมพ์เสริมและเกมแพดพิเศษอื่นๆ ได้

François Beaufort
François Beaufort

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

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

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

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

คำศัพท์

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

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

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

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

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

นอกจากนี้ คุณยังสามารถใช้คีย์ 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]}.`);
});

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

หากต้องการส่งรายงานเอาต์พุตไปยังอุปกรณ์ HID ให้ส่งรหัสรายงาน 8 บิตที่เชื่อมโยงกับรายงานเอาต์พุต (reportId) และไบต์เป็น BufferSource (data) ไปยัง device.sendReport() สัญญาที่ส่งกลับมาจะได้รับการแก้ไขเมื่อมีการส่งรายงาน หากอุปกรณ์ 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));

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

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

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

หากต้องการส่งรายงานฟีเจอร์ไปยังอุปกรณ์ HID ให้ส่งรหัสรายงาน 8 บิตที่เชื่อมโยงกับรายงานฟีเจอร์ (reportId) และไบต์เป็น BufferSource (data) ไปยัง device.sendFeatureReport() สัญญาที่ส่งกลับมาจะได้รับการแก้ไขเมื่อมีการส่งรายงาน หากอุปกรณ์ 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);
}

หากต้องการรับรายงานฟีเจอร์จากอุปกรณ์ HID ให้ส่งรหัสรายงาน 8 บิตที่เชื่อมโยงกับรายงานฟีเจอร์ (reportId) ไปยัง device.receiveFeatureReport() โดยสัญญาที่ส่งกลับมาจะแก้ไขด้วยออบเจ็กต์ 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 เพียงครั้งละ 1 เครื่องเป็นหลัก ในการตอบสนองต่อข้อความแจ้งของผู้ใช้ ผู้ใช้ต้องทำตามขั้นตอนที่มีอยู่เพื่อเลือกอุปกรณ์ HID ที่ต้องการ

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

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

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

ความคิดเห็น

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

บอกให้เราทราบเกี่ยวกับการออกแบบ API

มีบางอย่างเกี่ยวกับ API ที่ทำงานไม่ได้ตามที่คาดไว้ไหม หรือมีวิธีหรือคุณสมบัติที่ขาดหายไปที่คุณจำเป็นต้องใช้ในการนำแนวคิดของคุณไปปรับใช้

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

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

คุณพบข้อบกพร่องในการใช้งาน Chrome หรือไม่ หรือการใช้งานแตกต่างจาก ข้อกำหนดหรือไม่

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

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

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

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

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

ข้อความแสดงการยอมรับ

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