Continuamos a discutir os tópicos discutidos na Conferência Você Vai Amar o Frontend . Este artigo é inspirado em uma palestra de Michaela Lehr . Os vídeos da conferência estarão disponíveis esta semana, desde que haja slides . ( Vídeo já disponível )

Michaela Lehr conectou o vibrador ao navegador usando APIs da Web, a API da Web Bluetooth. Como o tráfego entre o aplicativo e o vibrador, ela descobriu que os comandos enviados são muito simples, por exemplo: vibrate: 5
. Depois, ensinando-o a vibrar ao som de gemidos de um vídeo que ela poderia encontrar na Internet, ela alcançou seus objetivos :)
Eu não tenho esses brinquedos e o design não se destina ao uso, mas existe um monitor de freqüência cardíaca Polar H10 que usa Bluetooth para transmitir dados. Na verdade, eu decidi "quebrar".
Não hackear
Antes de tudo, vale a pena entender como conectar o dispositivo ao navegador? Google ou Yandex, dependendo de suas inclinações: Web Bluetooth API
e no primeiro link , vemos um artigo sobre este tópico.
Infelizmente, tudo é muito mais simples e não há nada para farejar se você não deseja enviar algo para um dispositivo que não o deseja. No mesmo artigo, há até uma demonstração de um monitor de freqüência cardíaca conectado.

Isso me desencorajou, até o código fonte. Que horas se passaram?
Nós conectamos o dispositivo
Vamos criar index.html
com marcação típica:
<!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>
Como o meu monitor de frequência cardíaca certificado foi forjado em oficinas chinesas, mas em conformidade com os padrões, sua conexão e uso não devem causar dificuldades. Existe uma coisa - Atributos Genéricos (GATT) . Não entrei em muitos detalhes, mas se for simples, é um tipo de especificação que os dispositivos Bluetooth seguem. O GATT descreve suas propriedades e interações. Para o nosso projeto, é tudo o que precisamos saber. Um link útil para nós também é uma lista de serviços (dispositivos de fato). Encontrei o serviço de frequência cardíaca (org.bluetooth.service.heart_rate), que se parece com o que precisamos.
Para conectar o dispositivo ao navegador, o usuário deve interagir conscientemente com a interface do usuário. Portanto, é claro, segurança, considerando que, ao entrar na academia, meu monitor de batimentos cardíacos se conecta silenciosamente a tudo o que eu quero (ao mesmo tempo fiquei surpreso com isso). Obrigado, é claro, aos desenvolvedores do navegador, mas por quê ?! Oh, bem, não é difícil e não é tão nojento.
Vamos adicionar um botão e um manipulador à página no corpo de <body>
:
<button id="pair">Pair device</button> <script> window.onload = () => { const button = document.getElementById('pair') button.addEventListener('pointerup', function(event) { </script>
Como você pode ver, até agora não há Vue que prometi julgar pelo título. Mas eu não sei tudo sozinho e estou escrevendo um artigo ao longo do caminho. Então, o que estamos fazendo dessa maneira :)
Para conectar o dispositivo, precisamos usar navigator.bluetooth.requestDevice
. Este método pode aceitar uma matriz de filtros. Como nosso aplicativo funcionará na maioria das vezes apenas com monitores de freqüência cardíaca, filtraremos por eles:
navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] })
Abra o arquivo html em um navegador ou use browser-sync
:
browser-sync start --server --files ./
Estou usando um monitor de batimentos cardíacos e após alguns segundos o Chrome o encontrou:

Depois de encontrarmos o dispositivo que precisamos, é necessário ler os dados dele. Para fazer isso, conecte-o ao servidor GATT:
navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] }) .then((device) => { return device.gatt.connect(); })
Os dados que queremos ler estão nas Características do serviço. Os monitores de freqüência cardíaca têm apenas três características e estamos interessados em org.bluetooth.characteristic.heart_rate_measurement
Para considerar essa característica, precisamos obter o serviço principal. Honestamente, eu não sei por que. Talvez alguns dispositivos tenham vários sub-serviços. Em seguida, obtenha um depoimento e inscreva-se para receber notificações.
.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
função usada para analisar dados, a especificação de dados pode ser encontrada aqui - org.bluetooth.characteristic.heart_rate_measurement . Não vamos nos aprofundar nessa função em detalhes, tudo é banal por lá.
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; }
Tomou daqui: heartRateSensor.js
E assim, no console, vemos os dados que precisamos. Além da frequência cardíaca, meu monitor de freqüência cardíaca também mostra intervalos RR. Eu nunca pensei em como usá-los, este é o seu dever de casa :)
Código da página inteira <!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>
Desenho
O próximo passo é considerar o design do aplicativo. Ah, é claro, um artigo simples à primeira vista se transforma em uma tarefa não trivial. Eu gostaria de usar todo tipo de pathos e, na minha cabeça, há uma fila de artigos que preciso ler nas CSS Grids, Flexbox e Manipulações de animação CSS usando JS (a animação Pulse não é estática).
Esboço
Eu gosto de um design bonito, mas o designer é tão tão comigo.
Eu não tenho Photoshop, de alguma forma vamos sair do caminho.
Primeiro, vamos criar um novo projeto Vue.js usando Vue-cli
vue create heart-rate
Eu escolhi as configurações manuais e a primeira página de configurações fica assim:

Em seguida, escolha por si mesmo, mas eu tenho uma configuração do Airbnb, Jest e Sass.
Eu olhei para metade dos tutoriais do Wes Bos CSS Grids, eu recomendo que eles sejam gratuitos.
É hora de fazer o layout inicial. Não usaremos nenhuma estrutura CSS, nossa. Obviamente, também não pensamos em apoio.
Coruja desenho mágico
E então, antes de tudo, vamos definir nosso layout
. De fato, o aplicativo consistirá em duas partes. Vamos chamá-los assim - first
e second
. Na primeira parte, teremos uma representação numérica (batimentos por minuto), no segundo gráfico.
Eu decidi roubar o esquema de cores daqui .

Lançamos nosso aplicativo Vue se você ainda não o fez:
npm run serve
A ferramenta em si abrirá o navegador (ou não), há uma recarga quente e um link para testes externos. Eu imediatamente coloquei um telefone celular ao meu lado, porque estamos pensando no primeiro design para dispositivos móveis. Infelizmente, adicionei um PWA ao modelo e, no celular, o cache é limpo quando o navegador é fechado, mas isso acontece e ok é atualizado para salvar. Em geral, um momento incompreensível com o qual não entendi.
Primeiro, adicione utils.js
, com a nossa função de analisar valores, refatorando-o um pouco sob o slogan no projeto.
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, };
Em seguida, removemos todos os HelloWolrd.vue
desnecessários do HelloWolrd.vue
renomeando-os para HeartRate.vue
, esse componente será responsável por exibir os batimentos por minuto.
<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>
Crie HeartRateChart.vue
para o gráfico:
// HeartRateChart.vue <template> <div> chart </div> </template> <script> export default { name: 'HeartRateChart', props: { values: { type: Array, default: () => [], . . }, }, }; </script>
Atualizando 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
E, na verdade, mixins.scss
, embora exista apenas um mixin responsável pela cor do ícone e do texto exibindo batimentos por minuto.
@mixin heart-rate-gradient { background: -webkit-linear-gradient(
Aconteceu assim:
Dos pontos interessantes - são usadas variáveis nativas do CSS, mas mixins
do SCSS.
A página inteira é CSS Grid
:
display: grid; grid-gap: 1rem; height: 100vh; grid-template-rows: 1fr 1fr; grid-template-areas: "first" "second";
Como o flexbox
, o contêiner pai deve ter algum tipo de display
. Nesse caso, é grid
.
grid-gap
- um tipo de espaço entre columns
e rows
.
height: 100vh
- a altura de toda a viewport
, é necessário que fr
ocupe o espaço em altura total (2 partes de nossa aplicação).
grid-template-rows
- defina nosso modelo, fr
é uma unidade de açúcar que leva em consideração grid-gap
e outras propriedades que afetam o tamanho.
grid-template-areas
- em nosso exemplo, apenas semântico.
O Chrome ainda não forneceu ferramentas normais para a inspeção de CSS Grids:

Ao mesmo tempo no muff:

Agora, precisamos adicionar um manipulador de cliques de botão, semelhante ao que fizemos antes.
Adicione um manipulador:
// 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)); },
Não se esqueça que isso só funciona no chrome e somente no chrome no android :)
Em seguida, adicione um gráfico, usaremos o Chart.js
e um wrapper para o Vue.js
npm install vue-chartjs chart.js
A Polar possui 5 zonas de treinamento . Portanto, precisamos distinguir de alguma forma entre essas zonas e / ou armazená-las. Já temos heartRateData
. Para estética, criaremos o valor padrão do formulário:
heartRateData: [[], [], [], [], [], []],
Vamos dispersar os valores de acordo com 5 zonas:
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); } },
O ChartJS do Vue.js é usado da seguinte maneira:
// 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] } ] }) } }
Você importa o estilo de gráfico necessário, expande seu componente e, usando this.renderChart
exibe o gráfico.
No nosso caso, é necessário atualizar o agendamento à medida que novos dados chegam, para ocultar a exibição em um método updateChart
separado e chamá-lo de mounted
e usar o relógio para monitorar os 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>
Nossa aplicação está pronta. Mas, para não pular na frente da tela e chegar ao quinto nível, vamos adicionar um botão que irá gerar dados aleatórios para todos os cinco níveis para nós:
// 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; };
Resultado:

Conclusões
O uso da API da Web Bluetooth é muito simples. Há momentos em que é necessário ler dados usando operadores bit a bit, mas essa é uma área específica. Dos pontos negativos, é claro, é o apoio. No momento, é apenas chrome, e nos celulares chrome e apenas no android.

Fontes do Github
Demo