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

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

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

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

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

การใช้ 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 ขวาเพื่อให้คุณลองทำที่บ้านได้

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 ประเภทเดียวที่ส่งข้อมูลได้ทั้งสองทาง ซึ่งช่วยให้อุปกรณ์และแอปพลิเคชัน 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);
}

หากต้องการรับรายงานฟีเจอร์จากอุปกรณ์ 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 rule ใหม่ สร้างไฟล์ที่ /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 จะตรวจสอบการใช้งานของคอลเล็กชันระดับบนสุดแต่ละรายการ และหากคอลเล็กชันระดับบนสุดมีการใช้งานที่ได้รับการปกป้อง (เช่น แป้นพิมพ์ เมาส์ทั่วไป) เว็บไซต์จะไม่สามารถส่งและรับรายงานที่กําหนดไว้ในคอลเล็กชันนั้นได้ รายการการใช้งานที่ได้รับการคุ้มครองทั้งหมดเผยแพร่ต่อสาธารณะ

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

ความคิดเห็น

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

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

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

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

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

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

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

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

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

ส่งทวีตถึง @ChromiumDev โดยใช้แฮชแท็ก #WebHID และแจ้งให้เราทราบถึงตำแหน่งและวิธีใช้

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

ขอขอบคุณ

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