Kami terus membahas topik yang dibahas di Youend Gonna Love Frontend Conference. Artikel ini terinspirasi oleh ceramah oleh Michaela Lehr . Video konferensi akan tersedia minggu ini selama ada slide . ( Video sudah tersedia )

Michaela Lehr menghubungkan vibrator ke browser menggunakan Web API, yaitu Web Bluetooth API. Lalu lintas prosniferif antara aplikasi dan vibrator, ia menemukan bahwa perintah yang dikirim sangat sederhana, misalnya: vibrate: 5
. Kemudian, setelah mengajarinya untuk menggetarkan suara erangan dari video yang bisa dia temukan di Internet, dia mencapai tujuannya :)
Saya tidak punya mainan seperti itu dan desainnya tidak dimaksudkan untuk digunakan, tetapi ada monitor detak jantung Polar H10 yang menggunakan Bluetooth untuk mengirimkan data. Sebenarnya, saya memutuskan untuk "memecahkannya."
Tidak ada peretasan
Pertama-tama, perlu dipahami bagaimana menghubungkan perangkat ke browser? Google atau Yandex, tergantung pada kecenderungan Anda: Web Bluetooth API
, dan pada tautan pertama kami melihat artikel tentang topik ini.
Sayangnya, semuanya jauh lebih sederhana dan tidak ada yang mengendus jika Anda tidak ingin mengirim sesuatu ke perangkat yang tidak menginginkannya. Dalam artikel yang sama bahkan ada demonstrasi monitor detak jantung yang terhubung.

Itu membuat saya putus asa, bahkan kode sumbernya. Jam berapa sekarang?
Kami menghubungkan perangkat
Mari kita buat index.html
dengan markup khas:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> </body> </html>
Karena perangkat monitor detak jantung saya yang tersertifikasi dipalsukan di bengkel-bengkel Cina, tetapi sesuai dengan standar, koneksi dan penggunaannya seharusnya tidak menyebabkan kesulitan. Ada hal seperti itu - Atribut Generik (GATT) . Saya tidak membahas banyak detail, tetapi jika sederhana, itu semacam spesifikasi yang diikuti oleh perangkat Bluetooth. GATT menjelaskan sifat dan interaksinya. Untuk proyek kami, ini semua yang perlu kami ketahui. Tautan yang bermanfaat bagi kami juga merupakan daftar layanan (sebenarnya perangkat). Lalu saya menemukan layanan Denyut Jantung (org.bluetooth.service.heart_rate) yang terlihat seperti yang kita butuhkan.
Untuk menghubungkan perangkat ke browser, pengguna harus secara sadar berinteraksi dengan UI. Jadi-begitu, tentu saja, keamanan, mengingat bahwa ketika memasuki gym, monitor detak jantung saya secara diam-diam terhubung ke semua yang saya inginkan (pada suatu waktu saya terkejut dengan ini). Terima kasih tentu saja kepada pengembang peramban, tetapi mengapa?! Oh well, itu tidak sulit dan tidak terlalu menjijikkan.
Mari kita tambahkan tombol dan pawang ke halaman di badan <body>
:
<button id="pair">Pair device</button> <script> window.onload = () => { const button = document.getElementById('pair') button.addEventListener('pointerup', function(event) { </script>
Seperti yang Anda lihat, sejauh ini tidak ada Vue yang saya janjikan untuk dinilai berdasarkan judulnya. Tapi saya tidak tahu segalanya sendiri dan sedang menulis artikel di sepanjang jalan. Jadi apa yang kita lakukan dengan cara ini :)
Untuk menghubungkan perangkat, kita harus menggunakan navigator.bluetooth.requestDevice
. Metode ini dapat menerima berbagai filter. Karena aplikasi kami hanya akan berfungsi sebagian besar dengan monitor detak jantung, kami akan memfilternya:
navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] })
Buka file html di browser atau gunakan browser-sync
:
browser-sync start --server --files ./
Saya mengenakan monitor detak jantung dan setelah beberapa detik Chrome menemukannya:

Setelah kami menemukan perangkat yang kami butuhkan, perlu membaca data darinya. Untuk melakukan ini, sambungkan ke server GATT:
navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] }) .then((device) => { return device.gatt.connect(); })
Data yang ingin kita baca ada di Karakteristik Layanan. Monitor detak jantung hanya memiliki 3 karakteristik, dan kami tertarik dengan org.bluetooth.characteristic.heart_rate_measurement
Untuk mempertimbangkan karakteristik ini, kita perlu mendapatkan layanan utama. Jujur saya tidak tahu MENGAPA. Mungkin beberapa perangkat memiliki beberapa sub layanan. Kemudian dapatkan testimonial dan daftar pemberitahuan.
.then(server => { return server.getPrimaryService('heart_rate'); }) .then(service => { return service.getCharacteristic('heart_rate_measurement'); }) .then(characteristic => characteristic.startNotifications()) .then(characteristic => { characteristic.addEventListener( 'characteristicvaluechanged', handleCharacteristicValueChanged ); }) .catch(error => { console.log(error); }); function handleCharacteristicValueChanged(event) { var value = event.target.value; console.log(parseValue(value)); }
parseValue
fungsi yang digunakan untuk mengurai data, spesifikasi data dapat ditemukan di sini - org.bluetooth.characteristic.heart_rate_measurement . Kami tidak akan membahas fungsi ini secara rinci, semuanya basi di sana.
parseValue parseValue = (value) => { // Chrome 50+ DataView. value = value.buffer ? value : new DataView(value); let flags = value.getUint8(0); // let rate16Bits = flags & 0x1; let result = {}; let index = 1; // if (rate16Bits) { result.heartRate = value.getUint16(index, true); index += 2; } else { result.heartRate = value.getUint8(index); index += 1; } // RR let rrIntervalPresent = flags & 0x10; if (rrIntervalPresent) { let rrIntervals = []; for (; index + 1 < value.byteLength; index += 2) { rrIntervals.push(value.getUint16(index, true)); } result.rrIntervals = rrIntervals; } return result; }
Mengambil dari sini: heartRateSensor.js
Jadi, di konsol kita melihat data yang kita butuhkan. Selain detak jantung, monitor detak jantung saya juga menunjukkan interval RR. Saya tidak pernah menemukan cara menggunakannya, ini adalah pekerjaan rumah Anda :)
Kode halaman penuh <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <button id="pair">Pair device</button> <script> window.onload = () => { const button = document.getElementById('pair') parseValue = (value) => { // Chrome 50+ DataView. value = value.buffer ? value : new DataView(value); let flags = value.getUint8(0); // let rate16Bits = flags & 0x1; let result = {}; let index = 1; // if (rate16Bits) { result.heartRate = value.getUint16(index, true); index += 2; } else { result.heartRate = value.getUint8(index); index += 1; } // RR let rrIntervalPresent = flags & 0x10; if (rrIntervalPresent) { let rrIntervals = []; for (; index + 1 < value.byteLength; index += 2) { rrIntervals.push(value.getUint16(index, true)); } result.rrIntervals = rrIntervals; } return result; } button.addEventListener('pointerup', function(event) { navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] }) .then((device) => { return device.gatt.connect(); }) .then(server => { return server.getPrimaryService('heart_rate'); }) .then(service => { return service.getCharacteristic('heart_rate_measurement'); }) .then(characteristic => characteristic.startNotifications()) .then(characteristic => { characteristic.addEventListener('characteristicvaluechanged', handleCharacteristicValueChanged); }) .catch(error => { console.log(error); }); function handleCharacteristicValueChanged(event) { var value = event.target.value; console.log(parseValue(value)); // See https://github.com/WebBluetoothCG/demos/blob/gh-pages/heart-rate-sensor/heartRateSensor.js } }); } </script> </body> </html>
Desain
Langkah selanjutnya adalah mempertimbangkan desain aplikasi. Oh, tentu saja, sebuah artikel yang sederhana pada pandangan pertama berubah menjadi tugas yang tidak sepele. Saya ingin menggunakan semua jenis patho dan di kepala saya ada antrian artikel yang perlu saya baca di CSS Grids, Flexbox dan Manipulasi animasi CSS menggunakan JS (Animasi pulsa tidak statis).
Sketsa
Saya suka desain yang indah, tetapi desainernya begitu-begitu dengan saya.
Saya tidak punya Photoshop, kami entah bagaimana akan keluar dari jalan.
Pertama, mari kita buat proyek Vue.js baru menggunakan Vue-cli
vue create heart-rate
Saya memilih pengaturan manual dan halaman pengaturan pertama terlihat seperti ini:

Selanjutnya, pilih sendiri, tetapi saya memiliki konfigurasi Airbnb, Jest, dan Sass.
Saya melihat setengah dari tutorial Wes Bos CSS Grids, saya sarankan mereka gratis.
Saatnya melakukan tata letak awal. Kami tidak akan menggunakan kerangka kerja CSS apa pun, semua milik kami. Tentu saja, kami juga tidak memikirkan dukungan.
Sihir menggambar burung hantu
Jadi, pertama-tama, mari kita tentukan layout
kita. Bahkan, aplikasi akan terdiri dari dua bagian. Kami akan memanggil mereka itu - first
dan second
. Pada bagian pertama kita akan memiliki representasi numerik (denyut per menit), dalam grafik kedua.
Saya memutuskan untuk mencuri skema warna dari sini .

Kami meluncurkan aplikasi Vue kami jika Anda belum melakukannya:
npm run serve
Alat itu sendiri akan membuka browser (atau tidak), ada hot reload dan tautan untuk pengujian eksternal. Saya segera meletakkan ponsel di sebelah saya, karena kami berpikir tentang desain pertama ponsel. Sayangnya, saya menambahkan PWA ke templat, dan di ponsel, cache dibersihkan ketika browser ditutup, tetapi itu terjadi, dan ok diperbarui untuk menyimpan. Secara umum, saat yang tidak dapat dimengerti yang tidak saya mengerti.
Pertama, tambahkan utils.js
, dengan fungsi kami untuk menguraikan nilai, refactoring sedikit di bawah eslint dalam proyek.
utils.js export const parseHeartRateValues = (data) => { // Chrome 50+ DataView. const value = data.buffer ? data : new DataView(data); const flags = value.getUint8(0); // const rate16Bits = flags & 0x1; const result = {}; let index = 1; // if (rate16Bits) { result.heartRate = value.getUint16(index, true); index += 2; } else { result.heartRate = value.getUint8(index); index += 1; } // RR const rrIntervalPresent = flags & 0x10; if (rrIntervalPresent) { const rrIntervals = []; for (; index + 1 < value.byteLength; index += 2) { rrIntervals.push(value.getUint16(index, true)); } result.rrIntervals = rrIntervals; } return result; }; export default { parseHeartRateValues, };
Lalu kami menghapus semua yang tidak perlu dari HelloWolrd.vue
mengubah nama menjadi HeartRate.vue
, komponen ini akan bertanggung jawab untuk menampilkan ketukan per menit.
<template> <div> <span>{{value}}</span> </div> </template> <script> export default { name: 'HeartRate', props: { </script> // SCSS <style scoped lang="scss"> @import '../styles/mixins'; div { @include heart-rate-gradient; font-size: var(--heart-font-size); // } </style>
Buat HeartRateChart.vue
untuk bagan:
// HeartRateChart.vue <template> <div> chart </div> </template> <script> export default { name: 'HeartRateChart', props: { values: { type: Array, default: () => [], . . }, }, }; </script>
Memperbarui App.vue
:
Aplikasi <template> <div class=app> <div class=heart-rate-wrapper> <HeartRate v-if=heartRate :value=heartRate /> <i v-else class="fas fa-heartbeat"></i> <div> <button v-if=!heartRate class=blue>Click to start</button> </div> </div> <div class=heart-rate-chart-wrapper> <HeartRateChart :values=heartRateData /> </div> </div> </template> <script> import HeartRate from './components/HeartRate.vue'; import HeartRateChart from './components/HeartRateChart.vue'; import { parseHeartRateValues } from './utils'; export default { name: 'app', components: { HeartRate, HeartRateChart, }, data: () => ({ heartRate: 0, heartRateData: [], }), methods: { handleCharacteristicValueChanged(e) { this.heartRate = parseHeartRateValues(e.target.value).heartRate; }, }, }; </script> <style lang="scss"> @import './styles/mixins'; html, body { margin: 0px; } :root { // COLORS
Dan sebenarnya mixins.scss
, sementara hanya ada satu mixin yang bertanggung jawab atas warna ikon dan teks yang menampilkan ketukan per menit.
@mixin heart-rate-gradient { background: -webkit-linear-gradient(
Ternyata seperti ini:
Dari poin yang menarik - Variabel CSS asli digunakan, tetapi mixins
dari SCSS.
Seluruh halaman adalah CSS Grid
:
display: grid; grid-gap: 1rem; height: 100vh; grid-template-rows: 1fr 1fr; grid-template-areas: "first" "second";
Seperti flexbox
, wadah induk harus memiliki semacam display
. Dalam hal ini, itu adalah grid
.
grid-gap
- semacam ruang antara columns
dan rows
.
height: 100vh
- ketinggian seluruh viewport
, perlu fr
menempati ruang dengan ketinggian penuh (2 bagian dari aplikasi kami).
grid-template-rows
- mendefinisikan template kami, fr
adalah unit gula yang memperhitungkan grid-gap
akun dan properti lain yang memengaruhi ukuran.
grid-template-areas
- dalam contoh kita, hanya semantik.
Chrome belum mengirimkan alat normal untuk inspeksi CSS Grids:

Pada saat yang sama di muff:

Sekarang kita perlu menambahkan handler klik tombol, mirip dengan bagaimana kita melakukannya sebelumnya.
Tambahkan penangan:
// App.vue <button v-if=!heartRate @click=onClick class=blue>Click to start</button>
// Methods: {} onClick() { 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', this.handleCharacteristicValueChanged.bind(this))) .catch(error => console.log(error)); },
Jangan lupa bahwa ini hanya berfungsi di chrome dan hanya di chrome di android :)
Selanjutnya, tambahkan bagan, kita akan menggunakan Chart.js
dan pembungkus untuk Vue.js
npm install vue-chartjs chart.js
Polar memiliki 5 zona pelatihan . Karena itu, kita perlu membedakan antara zona-zona ini dan / atau menyimpannya. Kami sudah memiliki heartRateData
. Untuk estetika, kami membuat nilai default formulir:
heartRateData: [[], [], [], [], [], []],
Kami akan menyebarkan nilai menurut 5 zona:
pushData(index, value) { this.heartRateData[index].push({ x: Date.now(), y: value }); this.heartRateData = [...this.heartRateData]; }, handleCharacteristicValueChanged(e) { const value = parseHeartRateValues(e.target.value).heartRate; this.heartRate = value; switch (value) { case value > 104 && value < 114: this.pushData(1, value); break; case value > 114 && value < 133: this.pushData(2, value); break; case value > 133 && value < 152: this.pushData(3, value); break; case value > 152 && value < 172: this.pushData(4, value); break; case value > 172: this.pushData(5, value); break; default: this.pushData(0, value); } },
Vue.js ChartJS digunakan sebagai berikut:
// Example.js import { Bar } from 'vue-chartjs' export default { extends: Bar, mounted () { this.renderChart({ labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], datasets: [ { label: 'GitHub Commits', backgroundColor: '#f87979', data: [40, 20, 12, 39, 10, 40, 39, 80, 40, 20, 12, 11] } ] }) } }
Anda mengimpor gaya bagan yang diperlukan, memperluas komponen Anda, dan menggunakan this.renderChart
menampilkan bagan.
Dalam kasus kami, perlu untuk memperbarui jadwal ketika data baru tiba, jadi kami akan menyembunyikan tampilan dalam metode updateChart
terpisah dan menyebutnya pada mounted
dan menggunakan arloji untuk memantau values
:
HeartRateChart.vue <script> import { Scatter } from 'vue-chartjs'; export default { extends: Scatter, name: 'HeartRateChart', props: { values: { type: Array, default: () => [[], [], [], [], [], []], }, }, watch: { values() { this.updateChart(); }, }, mounted() { this.updateChart(); }, methods: { updateChart() { this.renderChart({ datasets: [ { label: 'Chilling', data: this.values[0], backgroundColor: '#4f775c', borderColor: '#4f775c', showLine: true, fill: false, }, { label: 'Very light', data: this.values[1], backgroundColor: '#465f9b', borderColor: '#465f9b', showLine: true, fill: false, }, { label: 'Light', data: this.values[2], backgroundColor: '#4e4491', borderColor: '#4e4491', showLine: true, fill: false, }, { label: 'Moderate', data: this.values[3], backgroundColor: '#6f2499', borderColor: '#6f2499', showLine: true, fill: false, }, { label: 'Hard', data: this.values[4], backgroundColor: '#823e62', borderColor: '#823e62', showLine: true, fill: false, }, { label: 'Maximum', data: this.values[5], backgroundColor: '#8a426f', borderColor: '#8a426f', showLine: true, fill: false, }, ], }, { animation: false, responsive: true, maintainAspectRatio: false, elements: { point: { radius: 0, }, }, scales: { xAxes: [{ display: false, }], yAxes: [{ ticks: { beginAtZero: true, fontColor: '#394365', }, gridLines: { color: '#2a334e', }, }], }, }); }, }, }; </script>
Aplikasi kita sudah siap. Tapi, agar tidak melompat di depan layar dan membawa diri kita ke level 5, mari kita tambahkan tombol yang akan menghasilkan data acak untuk semua 5 level untuk kita:
// App.vue <div> <button v-if=!heartRate @click=onClickTest class=blue>Test dataset</button> </div> ... import data from './__mock__/data'; ... onClickTest() { this.heartRateData = [ data(300, 60, 100), data(300, 104, 114), data(300, 133, 152), data(300, 152, 172), data(300, 172, 190), ]; this.heartRate = 73; },
// __mock__/date.js const getRandomIntInclusive = (min, max) => Math.floor(Math.random() * ((Math.floor(max) - Math.ceil(min)) + 1)) + Math.ceil(min); export default (count, from, to) => { const array = []; for (let i = 0; i < count; i += 1) { array.push({ y: getRandomIntInclusive(from, to), x: i }); } return array; };
Hasil:

Kesimpulan
Menggunakan Web Bluetooth API sangat sederhana. Ada saat-saat dengan kebutuhan untuk membaca data menggunakan operator bitwise, tetapi ini adalah area tertentu. Tentu saja minusnya adalah dukungan. Saat ini hanya chrome, dan pada ponsel chrome dan hanya pada android.

Sumber Github
Demo