Wir diskutieren weiterhin Themen, die auf der You Gonna Love Frontend- Konferenz diskutiert wurden. Dieser Artikel ist inspiriert von einem Vortrag von Michaela Lehr . Konferenzvideos werden diese Woche verfügbar sein, solange Folien vorhanden sind. ( Video bereits verfügbar )

Michaela Lehr hat den Vibrator über Web-APIs, nämlich die Web-Bluetooth-API, mit dem Browser verbunden. Prosniferif Verkehr zwischen der Anwendung und dem Vibrator, stellte sie fest, dass die gesendeten Befehle sehr einfach sind, zum Beispiel: vibrate: 5
. Nachdem sie ihm beigebracht hatte, mit dem Stöhnen eines Videos, das sie im Internet finden konnte, zu vibrieren, erreichte sie ihre Ziele :)
Ich habe kein solches Spielzeug und das Design ist nicht für die Verwendung vorgesehen, aber es gibt einen Polar H10- Herzfrequenzmesser, der Bluetooth zur Datenübertragung verwendet. Eigentlich habe ich beschlossen, "es zu knacken".
Kein Hacken
Zunächst lohnt es sich zu verstehen, wie das Gerät mit dem Browser verbunden wird. Google oder Yandex, abhängig von Ihren Neigungen: Web Bluetooth API
, und auf dem ersten Link sehen wir einen Artikel zu diesem Thema.
Leider ist alles viel einfacher und es gibt nichts zu schnüffeln, wenn Sie etwas nicht an ein Gerät senden möchten, das es nicht möchte. Im selben Artikel wird sogar ein angeschlossener Herzfrequenzmesser demonstriert.

Es hat mich wild entmutigt, sogar der Quellcode ist. Welche Zeit ist vergangen?
Wir verbinden das Gerät
Lassen Sie uns index.html
mit typischem Markup erstellen:
<!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>
Da mein zertifiziertes Herzfrequenzmessgerät in chinesischen Werkstätten geschmiedet wurde, aber den Standards entspricht, sollte seine Verbindung und Verwendung keine Schwierigkeiten verursachen. Es gibt so etwas - generische Attribute (GATT) . Ich habe nicht viel ins Detail gegangen, aber wenn es einfach ist, ist es eine Art Spezifikation, der Bluetooth-Geräte folgen. Das GATT beschreibt ihre Eigenschaften und Wechselwirkungen. Für unser Projekt ist dies alles, was wir wissen müssen. Ein nützlicher Link für uns ist auch eine Liste von Diensten (Geräte in der Tat). Dann habe ich den Herzfrequenzdienst (org.bluetooth.service.heart_rate) gefunden, der so aussieht, wie wir ihn brauchen.
Um das Gerät mit dem Browser zu verbinden, muss der Benutzer bewusst mit der Benutzeroberfläche interagieren. So lala natürlich Sicherheit, wenn man bedenkt, dass mein Herzfrequenzmesser beim Betreten des Fitnessraums lautlos eine Verbindung zu allem herstellt, was ich will (einmal war ich davon überrascht). Danke natürlich an die Browser-Entwickler, aber warum ?! Na ja, es ist nicht schwierig und nicht so ekelhaft.
Fügen wir der Seite im Hauptteil von <body>
eine Schaltfläche und einen Handler hinzu:
<button id="pair">Pair device</button> <script> window.onload = () => { const button = document.getElementById('pair') button.addEventListener('pointerup', function(event) { </script>
Wie Sie sehen, gibt es bisher keinen Vue, den ich versprochen habe, nach dem Titel zu urteilen. Aber ich weiß nicht alles selbst und schreibe unterwegs einen Artikel. Also, was machen wir so :)
Um das Gerät zu verbinden, müssen wir navigator.bluetooth.requestDevice
. Diese Methode kann ein Array von Filtern akzeptieren. Da unsere Anwendung größtenteils nur mit Herzfrequenzmessgeräten funktioniert, werden wir nach diesen filtern:
navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] })
Öffnen Sie die HTML-Datei in einem Browser oder verwenden Sie die browser-sync
:
browser-sync start --server --files ./
Ich trage einen Herzfrequenzmesser und nach einigen Sekunden hat Chrome ihn gefunden:

Nachdem wir das benötigte Gerät gefunden haben, müssen Daten daraus gelesen werden. Verbinden Sie dazu den GATT-Server:
navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] }) .then((device) => { return device.gatt.connect(); })
Die Daten, die wir lesen möchten, befinden sich in den Serviceeigenschaften. Herzfrequenzmesser haben nur 3 Eigenschaften, und wir sind an org.bluetooth.characteristic.heart_rate_measurement interessiert
Um dieses Merkmal zu berücksichtigen, müssen wir den Hauptdienst erhalten. Ehrlich gesagt weiß ich nicht WARUM. Möglicherweise haben einige Geräte mehrere Unterdienste. Dann holen Sie sich ein Zeugnis und melden Sie sich für Benachrichtigungen an.
.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
Funktion, mit der Daten analysiert werden. Die Datenspezifikation finden Sie hier - org.bluetooth.characteristic.heart_rate_measurement . Wir werden nicht im Detail auf diese Funktion eingehen, da ist dort alles banal.
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; }
Nahm von hier: heartRateSensor.js
Und so sehen wir in der Konsole die Daten, die wir brauchen. Neben der Herzfrequenz zeigt mein Herzfrequenzmesser auch RR-Intervalle an. Ich habe nie herausgefunden, wie man sie benutzt, das sind deine Hausaufgaben :)
Ganzseitiger Code <!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>
Design
Der nächste Schritt besteht darin, das Design der Anwendung zu berücksichtigen. Natürlich wird ein Artikel, der auf den ersten Blick einfach ist, zu einer nicht trivialen Aufgabe. Ich möchte alle Arten von Pathos verwenden und in meinem Kopf befindet sich eine Reihe von Artikeln, die ich in CSS Grids lesen muss. Flexbox und Manipulationen von CSS-Animationen mit JS (Pulsanimation ist nicht statisch).
Skizze
Ich mag schönes Design, aber der Designer ist so lala mit mir.
Ich habe kein Photoshop, wir werden irgendwie aus dem Weg gehen.
Lassen Sie uns zunächst ein neues Vue.js-Projekt mit Vue-cli erstellen
vue create heart-rate
Ich habe manuelle Einstellungen gewählt und die erste Einstellungsseite sieht folgendermaßen aus:

Als nächstes wählen Sie selbst, aber ich habe eine Airbnb-, Jest- und Sass-Konfiguration.
Ich habe mir die Hälfte der Wes Bos CSS Grids-Tutorials angesehen. Ich empfehle, dass sie kostenlos sind.
Es ist Zeit, das anfängliche Layout zu erstellen. Wir werden keine eigenen CSS-Frameworks verwenden. Natürlich denken wir auch nicht an Unterstützung.
Eule, die Magie zeichnet
Lassen Sie uns zunächst unser layout
definieren. Tatsächlich besteht die Anwendung aus zwei Teilen. Wir werden sie so nennen - first
und second
. Im ersten Teil haben wir eine numerische Darstellung (Schläge pro Minute) im zweiten Diagramm.
Ich beschloss, das Farbschema von hier zu stehlen.

Wir starten unsere Vue-Anwendung, falls Sie dies noch nicht getan haben:
npm run serve
Das Tool selbst öffnet den Browser (oder nicht), es gibt ein Hot-Reload und einen Link für externe Tests. Ich habe sofort ein Handy neben mich gestellt, weil wir über Mobile First Design nachdenken. Leider habe ich der Vorlage eine PWA hinzugefügt, und auf dem Mobiltelefon wird der Cache beim Schließen des Browsers bereinigt, aber es passiert, und ok wird zum Speichern aktualisiert. Im Allgemeinen ein unverständlicher Moment, mit dem ich nicht verstanden habe.
utils.js
Sie zunächst utils.js
, mit unserer Funktion, Werte zu analysieren und sie im Projekt unter eslint ein wenig umzugestalten.
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, };
Dann entfernen wir alles Unnötige aus HelloWolrd.vue
es in HeartRate.vue
. Diese Komponente ist für die Anzeige von Beats pro Minute verantwortlich.
<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>
Erstellen Sie HeartRateChart.vue
für das Diagramm:
// HeartRateChart.vue <template> <div> chart </div> </template> <script> export default { name: 'HeartRateChart', props: { values: { type: Array, default: () => [], . . }, }, }; </script>
App.vue
:
App.vue <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
Und tatsächlich mixins.scss
, während es nur ein mixins.scss
gibt, das für die Farbe des Symbols und des Texts verantwortlich ist, der die Beats pro Minute anzeigt.
@mixin heart-rate-gradient { background: -webkit-linear-gradient(
Es stellte sich so heraus:
Von den interessanten Punkten werden native CSS-Variablen verwendet, aber mixins
von SCSS.
Die ganze Seite ist CSS Grid
:
display: grid; grid-gap: 1rem; height: 100vh; grid-template-rows: 1fr 1fr; grid-template-areas: "first" "second";
Wie die flexbox
muss der übergeordnete Container eine Art display
. In diesem Fall handelt es sich um ein grid
.
grid-gap
- eine Art Leerzeichen zwischen columns
und rows
.
height: 100vh
- Die Höhe des gesamten viewport
fr
den Raum in voller Höhe einnehmen (2 Teile unserer Anwendung).
grid-template-rows
- Definieren Sie unsere Vorlage. fr
ist eine Zuckereinheit, die die grid-gap
und andere Eigenschaften berücksichtigt, die sich auf die Größe auswirken.
grid-template-areas
- in unserem Beispiel nur semantisch.
Chrome hat noch keine normalen Tools für die CSS-Grids-Überprüfung geliefert:

Zur gleichen Zeit im Muff:

Jetzt müssen wir einen Button-Click-Handler hinzufügen, ähnlich wie zuvor.
Fügen Sie einen Handler hinzu:
// 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)); },
Vergiss nicht, dass dies nur in Chrom und nur in Chrom auf Android funktioniert :)
Als nächstes fügen Sie ein Diagramm hinzu, wir werden Chart.js
und einen Wrapper für Vue.js verwenden
npm install vue-chartjs chart.js
Polar hat 5 Trainingszonen . Daher müssen wir diese Zonen irgendwie unterscheiden und / oder speichern. Wir haben bereits heartRateData
. Aus Gründen der Ästhetik legen wir den Standardwert des Formulars fest:
heartRateData: [[], [], [], [], [], []],
Wir streuen Werte nach 5 Zonen:
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 werden wie folgt verwendet:
// 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] } ] }) } }
Sie importieren den erforderlichen Diagrammstil, erweitern Ihre Komponente und zeigen mit this.renderChart
das Diagramm an.
In unserem Fall ist es erforderlich, den Zeitplan zu aktualisieren, updateChart
neue Daten eintreffen. updateChart
verbergen wir die Anzeige in einer separaten updateChart
Methode und rufen sie bei mounted
und verwenden die Uhr, um die values
zu überwachen:
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>
Unsere Bewerbung ist fertig. Um jedoch nicht vor den Bildschirm zu springen und uns auf die 5. Ebene zu bringen, fügen wir eine Schaltfläche hinzu, die für uns zufällige Daten für alle 5 Ebenen generiert:
// 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; };
Ergebnis:

Schlussfolgerungen
Die Verwendung der Web-Bluetooth-API ist sehr einfach. Es gibt Momente, in denen Daten mit bitweisen Operatoren gelesen werden müssen, dies ist jedoch ein bestimmter Bereich. Von den Minuspunkten ist natürlich die Unterstützung. Im Moment ist es nur Chrom und auf Mobiltelefonen Chrom und nur auf Android.

Github-Quellen
Demo