การสื่อสารกับอุปกรณ์บลูทูธผ่าน JavaScript

Web Bluetooth API ช่วยให้เว็บไซต์สื่อสารกับอุปกรณ์บลูทูธได้

François Beaufort
François Beaufort

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

ที่ผ่านมาเราให้คุณโต้ตอบกับอุปกรณ์บลูทูธได้ สำหรับแอปเฉพาะแพลตฟอร์ม Web Bluetooth API มีเป้าหมายเพื่อเปลี่ยนการตั้งค่านี้และ นำมาสู่เว็บเบราว์เซอร์ด้วย

ก่อนที่จะเริ่ม

เอกสารนี้จะถือว่าคุณมีความรู้พื้นฐานเกี่ยวกับบลูทูธต่ำ การทำงานของพลังงาน (BLE) และโปรไฟล์แอตทริบิวต์ทั่วไป

แม้ว่าข้อกำหนดเฉพาะของ Web Bluetooth API จะยังไม่เป็นที่สิ้นสุด แต่ข้อกำหนด ผู้เขียนกำลังมองหานักพัฒนาซอฟต์แวร์ที่มีความกระตือรือร้น เพื่อทดลองใช้ API นี้ แสดงความคิดเห็นเกี่ยวกับข้อกำหนดและความคิดเห็นเกี่ยวกับการติดตั้งใช้งาน

ส่วนย่อยของ Web Bluetooth API มีอยู่ใน ChromeOS, Chrome สำหรับ Android 6.0, Mac (Chrome 56) และ Windows 10 (Chrome 70) ซึ่งหมายความว่าคุณควรสามารถ เพื่อขอและเชื่อมต่ออุปกรณ์บลูทูธพลังงานต่ำที่อยู่ใกล้เคียง อ่าน/เขียนลักษณะของบลูทูธ รับการแจ้งเตือน GATT ทราบ เมื่ออุปกรณ์บลูทูธถูกตัดการเชื่อมต่อ หรือแม้แต่อ่านและเขียนไปยัง ข้อบ่งชี้บลูทูธ โปรดดูตารางความเข้ากันได้กับเบราว์เซอร์ของ MDN สำหรับข้อมูลเพิ่มเติม

สำหรับ Linux และ Windows เวอร์ชันก่อนหน้า ให้เปิดใช้ การแจ้งว่าไม่เหมาะสม #experimental-web-platform-features ใน about://flags

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

เพื่อรับความคิดเห็นมากที่สุดเท่าที่จะเป็นไปได้จากนักพัฒนาซอฟต์แวร์ที่ใช้เว็บ Bluetooth API ในช่องดังกล่าว Chrome ได้เพิ่มฟีเจอร์นี้ไว้ใน Chrome ก่อนหน้านี้แล้ว 53 เป็นช่วงทดลองใช้จากต้นทางสำหรับ ChromeOS, Android และ Mac

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

ข้อกำหนดด้านความปลอดภัย

หากต้องการทำความเข้าใจเกี่ยวกับข้อดีและข้อเสียของความปลอดภัย ฉันขอแนะนำให้ตั้งค่า Web Bluetooth Security โมเดลโพสต์จาก Jeffrey Yasskin วิศวกรซอฟต์แวร์ในทีม Chrome กำลังทำตามข้อกำหนดของ Web Bluetooth API

HTTPS เท่านั้น

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

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

เป็นฟีเจอร์ความปลอดภัย การค้นหาอุปกรณ์บลูทูธที่มี navigator.bluetooth.requestDevice ต้องทริกเกอร์ด้วยท่าทางสัมผัสของผู้ใช้ ผ่านการแตะหรือการคลิกเมาส์ เรากำลังพูดถึงการฟัง pointerup, click และ touchend เหตุการณ์

button.addEventListener('pointerup', function(event) {
  // Call navigator.bluetooth.requestDevice
});

ทำความเข้าใจโค้ด

Web Bluetooth API อาศัย Promises ของ JavaScript เป็นหลัก หากไม่ใช่ คุ้นเคยกับสิ่งเหล่านี้ ดูบทแนะนำเรื่องสัญญาที่ยอดเยี่ยมนี้ อีกอย่าง () => {} คือ ฟังก์ชันลูกศร ECMAScript 2015

ขออุปกรณ์บลูทูธ

ข้อกำหนดของ Web Bluetooth API เวอร์ชันนี้อนุญาตให้เว็บไซต์ที่ทำงานใน บทบาทส่วนกลาง เพื่อเชื่อมต่อกับเซิร์ฟเวอร์ GATT ระยะไกลผ่านการเชื่อมต่อ BLE ทั้งนี้ สนับสนุนการสื่อสารระหว่างอุปกรณ์ที่ใช้บลูทูธ 4.0 ขึ้นไป

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

ข้อความแจ้งผู้ใช้อุปกรณ์บลูทูธ

ฟังก์ชัน navigator.bluetooth.requestDevice() รับออบเจ็กต์ที่จำเป็นซึ่ง กำหนดตัวกรอง ตัวกรองเหล่านี้ใช้เพื่อแสดงผลอุปกรณ์ที่ตรงกับ บริการ Bluetooth GATT ที่โฆษณา และ/หรือชื่ออุปกรณ์

ตัวกรองบริการ

ตัวอย่างเช่น หากต้องการขออุปกรณ์บลูทูธที่โฆษณา บลูทูธ GATT บริการแบตเตอรี่:

navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })
.then(device => { /* … */ })
.catch(error => { console.error(error); });

หากบริการบลูทูธ GATT ไม่อยู่ในรายการบลูทูธมาตรฐาน บริการ GATT คุณอาจระบุ UUID ของบลูทูธแบบเต็มหรือ รูปแบบ 16 หรือ 32 บิต

navigator.bluetooth.requestDevice({
  filters: [{
    services: [0x1234, 0x12345678, '99999999-0000-1000-8000-00805f9b34fb']
  }]
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

ตัวกรองชื่อ

นอกจากนั้นยังขออุปกรณ์บลูทูธตามชื่ออุปกรณ์ที่โฆษณาอยู่ได้ด้วย ด้วยคีย์ตัวกรอง name หรือแม้แต่คำนำหน้าชื่อนี้ด้วย namePrefix แป้นตัวกรอง โปรดทราบว่าในกรณีนี้คุณต้องกำหนด optionalServices เพื่อให้สามารถเข้าถึงบริการที่ไม่รวมอยู่ใน ตัวกรองบริการ หากไม่ดำเนินการดังกล่าว คุณจะได้รับข้อผิดพลาดในภายหลังเมื่อพยายามเข้าถึง ให้พวกเขา

navigator.bluetooth.requestDevice({
  filters: [{
    name: 'Francois robot'
  }],
  optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

ตัวกรองข้อมูลผู้ผลิต

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

// Filter Bluetooth devices from Google company with manufacturer data bytes
// that start with [0x01, 0x02].
navigator.bluetooth.requestDevice({
  filters: [{
    manufacturerData: [{
      companyIdentifier: 0x00e0,
      dataPrefix: new Uint8Array([0x01, 0x02])
    }]
  }],
  optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

มาสก์ยังใช้กับคํานําหน้าข้อมูลเพื่อจับคู่รูปแบบบางอย่างใน จากผู้ผลิต ดูข้อมูลเพิ่มเติมได้ในคำอธิบายตัวกรองข้อมูลบลูทูธ และอีกมากมาย

ตัวกรองการยกเว้น

ตัวเลือก exclusionFilters ใน navigator.bluetooth.requestDevice() ช่วยให้ คุณจะยกเว้นอุปกรณ์บางเครื่องจากเครื่องมือเลือกเบราว์เซอร์ สามารถใช้เพื่อยกเว้น อุปกรณ์ที่ตรงกับตัวกรองที่กว้างกว่าแต่ไม่รองรับ

// Request access to a bluetooth device whose name starts with "Created by".
// The device named "Created by Francois" has been reported as unsupported.
navigator.bluetooth.requestDevice({
  filters: [{
    namePrefix: "Created by"
  }],
  exclusionFilters: [{
    name: "Created by Francois"
  }],
  optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

ไม่ใช้ฟิลเตอร์

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

navigator.bluetooth.requestDevice({
  acceptAllDevices: true,
  optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

เชื่อมต่อกับอุปกรณ์บลูทูธ

แล้วตอนนี้คุณมี BluetoothDevice แล้วหรือยัง มาเชื่อมต่อ เซิร์ฟเวอร์ GATT แบบบลูทูธระยะไกลที่มีบริการและลักษณะเฉพาะ คำจำกัดความ

navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })
.then(device => {
  // Human-readable name of the device.
  console.log(device.name);

  // Attempts to connect to remote GATT Server.
  return device.gatt.connect();
})
.then(server => { /* … */ })
.catch(error => { console.error(error); });

อ่านลักษณะของบลูทูธ

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

ในตัวอย่างด้านล่าง battery_level คือ ระดับแบตเตอรี่ที่ได้มาตรฐาน ลักษณะเฉพาะ

navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })
.then(device => device.gatt.connect())
.then(server => {
  // Getting Battery Service…
  return server.getPrimaryService('battery_service');
})
.then(service => {
  // Getting Battery Level Characteristic…
  return service.getCharacteristic('battery_level');
})
.then(characteristic => {
  // Reading Battery Level…
  return characteristic.readValue();
})
.then(value => {
  console.log(`Battery percentage is ${value.getUint8(0)}`);
})
.catch(error => { console.error(error); });

หากคุณใช้ลักษณะบลูทูธ GATT แบบกำหนดเอง คุณจะใส่ฟังก์ชัน UUID ของบลูทูธเต็มรูปแบบ หรือแบบฟอร์ม 16 หรือ 32 บิตสั้นๆ service.getCharacteristic

โปรดทราบว่าคุณยังสามารถเพิ่ม Listener เหตุการณ์ characteristicvaluechanged ใน ลักษณะเฉพาะในการจัดการการอ่านค่า โปรดดูลักษณะการอ่าน ตัวอย่างค่าที่เปลี่ยนแปลงเพื่อดูวิธีจัดการกับ GATT ที่กำลังจะเกิดขึ้น การแจ้งเตือนได้อีกด้วย

…
.then(characteristic => {
  // Set up event listener for when characteristic value changes.
  characteristic.addEventListener('characteristicvaluechanged',
                                  handleBatteryLevelChanged);
  // Reading Battery Level…
  return characteristic.readValue();
})
.catch(error => { console.error(error); });

function handleBatteryLevelChanged(event) {
  const batteryLevel = event.target.value.getUint8(0);
  console.log('Battery percentage is ' + batteryLevel);
}

เขียนไปยังลักษณะเฉพาะของบลูทูธ

การเขียนไปยังลักษณะเฉพาะของบลูทูธ GATT นั้นง่ายพอๆ กับการอ่าน คราวนี้ มาใช้จุดควบคุมอัตราการเต้นของหัวใจเพื่อรีเซ็ตค่าของพลังงานที่ใช้ไป เป็น 0 บนอุปกรณ์ตรวจสอบอัตราการเต้นของหัวใจ

ฉันสัญญาว่าที่นี่ไม่มีเวทมนตร์ ดูคำอธิบายทั้งหมดได้ในการควบคุมอัตราการเต้นของหัวใจ หน้าลักษณะของจุด

navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('heart_rate'))
.then(service => service.getCharacteristic('heart_rate_control_point'))
.then(characteristic => {
  // Writing 1 is the signal to reset energy expended.
  const resetEnergyExpended = Uint8Array.of(1);
  return characteristic.writeValue(resetEnergyExpended);
})
.then(_ => {
  console.log('Energy expended has been reset.');
})
.catch(error => { console.error(error); });

รับการแจ้งเตือน GATT

ตอนนี้มาดูวิธีรับการแจ้งเตือนเมื่อการวัดอัตราการเต้นของหัวใจ ลักษณะเฉพาะของอุปกรณ์

navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('heart_rate'))
.then(service => service.getCharacteristic('heart_rate_measurement'))
.then(characteristic => characteristic.startNotifications())
.then(characteristic => {
  characteristic.addEventListener('characteristicvaluechanged',
                                  handleCharacteristicValueChanged);
  console.log('Notifications have been started.');
})
.catch(error => { console.error(error); });

function handleCharacteristicValueChanged(event) {
  const value = event.target.value;
  console.log('Received ' + value);
  // TODO: Parse Heart Rate Measurement value.
  // See https://github.com/WebBluetoothCG/demos/blob/gh-pages/heart-rate-sensor/heartRateSensor.js
}

ตัวอย่างการแจ้งเตือนจะแสดงวิธีหยุดการแจ้งเตือนที่มี stopNotifications() และนำ characteristicvaluechanged ที่เพิ่มไว้ออกอย่างถูกต้อง Listener เหตุการณ์

ยกเลิกการเชื่อมต่อกับอุปกรณ์บลูทูธ

หากต้องการให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ดียิ่งขึ้น คุณอาจต้องฟังเหตุการณ์การขาดการเชื่อมต่อ และเชิญผู้ใช้ให้เชื่อมต่อใหม่

navigator.bluetooth.requestDevice({ filters: [{ name: 'Francois robot' }] })
.then(device => {
  // Set up event listener for when device gets disconnected.
  device.addEventListener('gattserverdisconnected', onDisconnected);

  // Attempts to connect to remote GATT Server.
  return device.gatt.connect();
})
.then(server => { /* … */ })
.catch(error => { console.error(error); });

function onDisconnected(event) {
  const device = event.target;
  console.log(`Device ${device.name} is disconnected.`);
}

คุณยังโทรหา device.gatt.disconnect() เพื่อยกเลิกการเชื่อมต่อเว็บแอปจาก อุปกรณ์บลูทูธ การดำเนินการนี้จะทริกเกอร์เหตุการณ์ gattserverdisconnected ที่มีอยู่ Listener ของคุณ โปรดทราบว่าการดำเนินการนี้จะไม่หยุดการสื่อสารของอุปกรณ์บลูทูธหากมี แอปกำลังสื่อสารกับอุปกรณ์บลูทูธอยู่แล้ว โปรดดูอุปกรณ์ ตัวอย่างการยกเลิกการเชื่อมต่อและตัวอย่างการเชื่อมต่อใหม่อัตโนมัติเพื่อเจาะลึกมากขึ้น

อ่านและเขียนไปยังข้อบ่งชี้บลูทูธ

ตัวบ่งชี้ GATT ของบลูทูธเป็นแอตทริบิวต์ที่อธิบายค่าลักษณะเฉพาะ คุณอ่านและเขียนรหัสผ่านได้ในลักษณะเดียวกับบลูทูธ GATT ลักษณะพิเศษ

มาดูวิธีอ่านคำอธิบายผู้ใช้ของการวัดผลกัน ระยะเวลาของเทอร์โมมิเตอร์สุขภาพของอุปกรณ์

ในตัวอย่างด้านล่าง health_thermometer คือบริการเทอร์โมมิเตอร์สุขภาพ measurement_interval ลักษณะของช่วงการวัด และ gatt.characteristic_user_descriptionคำอธิบายลักษณะเฉพาะของผู้ใช้ ข้อบ่งชี้

navigator.bluetooth.requestDevice({ filters: [{ services: ['health_thermometer'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('health_thermometer'))
.then(service => service.getCharacteristic('measurement_interval'))
.then(characteristic => characteristic.getDescriptor('gatt.characteristic_user_description'))
.then(descriptor => descriptor.readValue())
.then(value => {
  const decoder = new TextDecoder('utf-8');
  console.log(`User Description: ${decoder.decode(value)}`);
})
.catch(error => { console.error(error); });

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

navigator.bluetooth.requestDevice({ filters: [{ services: ['health_thermometer'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('health_thermometer'))
.then(service => service.getCharacteristic('measurement_interval'))
.then(characteristic => characteristic.getDescriptor('gatt.characteristic_user_description'))
.then(descriptor => {
  const encoder = new TextEncoder('utf-8');
  const userDescription = encoder.encode('Defines the time between measurements.');
  return descriptor.writeValue(userDescription);
})
.catch(error => { console.error(error); });

ตัวอย่าง การสาธิต และ Codelab

ตัวอย่างบลูทูธแบบเว็บทั้งหมดด้านล่างได้รับการทดสอบเรียบร้อยแล้ว เพื่อเพลิดเพลินกับฟีเจอร์เหล่านี้ เราขอแนะนำให้คุณติดตั้ง [BLE Peripheral Simulator แอป Android] ซึ่งจำลองอุปกรณ์ต่อพ่วง BLE พร้อมบริการแบตเตอรี่ซึ่งเป็นอัตราการเต้นของหัวใจ บริการ หรือบริการเทอร์โมมิเตอร์ด้านสุขภาพ

ผู้เริ่มต้น

การรวมการดำเนินการหลายรายการ

ดูการสาธิตการใช้เว็บบลูทูธที่มีการดูแลจัดการและCodelab เกี่ยวกับบลูทูธอย่างเป็นทางการ

ห้องสมุด

  • web-bluetooth-utils เป็นโมดูล npm ที่เพิ่มฟังก์ชันอำนวยความสะดวกบางอย่าง API
  • Shim สำหรับ Web Bluetooth API พร้อมใช้งานใน noble, Node.js BLE ที่ได้รับความนิยมสูงสุด โมดูลกลาง ซึ่งจะช่วยให้คุณใช้ Webpack/ใช้เบราว์เซอร์ Noble ได้โดยไม่จำเป็นต้อง สำหรับเซิร์ฟเวอร์ WebSocket หรือปลั๊กอินอื่นๆ
  • angular-web-bluetooth เป็นโมดูลสำหรับ Angular ที่ลบองค์ประกอบ Boilerplate ที่จำเป็นสำหรับการกำหนดค่า Web Bluetooth API

เครื่องมือ

เคล็ดลับ

หน้า Bluetooth Internals มีให้ใช้งานใน Chrome ที่ about://bluetooth-internals เพื่อให้คุณสามารถตรวจสอบทุกอย่างเกี่ยวกับสถานที่ใกล้เคียง อุปกรณ์บลูทูธ: สถานะ บริการ ลักษณะ และข้อบ่งชี้

ภาพหน้าจอของหน้าเว็บภายในเพื่อแก้ไขข้อบกพร่องของบลูทูธใน Chrome
หน้าภายในใน Chrome สำหรับแก้ไขข้อบกพร่องของอุปกรณ์บลูทูธ

และเราขอแนะนำให้ดูวิธีรายงานข้อบกพร่องเกี่ยวกับบลูทูธบนเว็บอย่างเป็นทางการ การแก้ไขข้อบกพร่องของบลูทูธอาจทำได้ยากในบางครั้ง

ขั้นตอนถัดไป

ตรวจสอบสถานะการใช้งานเบราว์เซอร์และแพลตฟอร์มก่อน เพื่อให้ทราบว่าส่วนใด ของ Web Bluetooth API ที่กำลังใช้อยู่

ถึงแม้ว่าจะยังไม่สมบูรณ์ แต่นี่คือตัวอย่างตัวอย่างของสิ่งที่จะเกิดขึ้นใน อนาคต:

  • การสแกนหาโฆษณา BLE ที่อยู่ใกล้เคียง จะเกิดขึ้นกับ navigator.bluetooth.requestLEScan()
  • เหตุการณ์ serviceadded ใหม่จะติดตามบริการ Bluetooth GATT ที่เพิ่งค้นพบ ในขณะที่ serviceremoved เหตุการณ์จะติดตามรายการที่นำออก servicechangedรายการใหม่ เหตุการณ์จะเริ่มทำงานเมื่อมีการเพิ่มลักษณะและ/หรือข้อบ่งชี้หรือ นำออกจากบริการบลูทูธ GATT แล้ว

แสดงการรองรับ API

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

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

แหล่งข้อมูล

กิตติกรรมประกาศ

ขอขอบคุณ Kayce Basques ที่เขียนรีวิวบทความนี้ รูปภาพหลักโดย SparkFun Electronics จากโบลเดอร์ สหรัฐอเมริกา