How to build a web app for your own BLE device: Habrahabr
importScripts('sw-toolbox.js');
toolbox.precache([
'companion.js',
'index.html',
'main.js',
'styles.css',
]);
#terminal div {
color: gray;
}
#terminal div.out {
color: red;
}
#terminal div.in {
color: blue;
}
// Получение ссылок на элементы UI
let connectButton = document.getElementById('connect');
let disconnectButton = document.getElementById('disconnect');
let terminalContainer = document.getElementById('terminal');
let sendForm = document.getElementById('send-form');
let inputField = document.getElementById('input');
// Подключение к устройству при нажатии на кнопку Connect
connectButton.addEventListener('click', function() {
connect();
});
// Отключение от устройства при нажатии на кнопку Disconnect
disconnectButton.addEventListener('click', function() {
disconnect();
});
// Обработка события отправки формы
sendForm.addEventListener('submit', function(event) {
event.preventDefault(); // Предотвратить отправку формы
send(inputField.value); // Отправить содержимое текстового поля
inputField.value = ''; // Обнулить текстовое поле
inputField.focus(); // Вернуть фокус на текстовое поле
});
// Кэш объекта выбранного устройства
let deviceCache = null;
// Кэш объекта характеристики
let characteristicCache = null;
// Промежуточный буфер для входящих данных
let readBuffer = '';
// Запустить выбор Bluetooth устройства и подключиться к выбранному
function connect() {
return (deviceCache ? Promise.resolve(deviceCache) :
requestBluetoothDevice()).
then(device => connectDeviceAndCacheCharacteristic(device)).
then(characteristic => startNotifications(characteristic)).
catch(error => log(error));
}
// Запрос выбора Bluetooth устройства
function requestBluetoothDevice() {
log('Requesting bluetooth device...');
return navigator.bluetooth.requestDevice({
filters: [{services: [0xFFE0]}],
}).
then(device => {
log('"' + device.name + '" bluetooth device selected');
deviceCache = device;
deviceCache.addEventListener('gattserverdisconnected',
handleDisconnection);
return deviceCache;
});
}
// Обработчик разъединения
function handleDisconnection(event) {
let device = event.target;
log('"' + device.name +
'" bluetooth device disconnected, trying to reconnect...');
connectDeviceAndCacheCharacteristic(device).
then(characteristic => startNotifications(characteristic)).
catch(error => log(error));
}
// Подключение к определенному устройству, получение сервиса и характеристики
function connectDeviceAndCacheCharacteristic(device) {
if (device.gatt.connected && characteristicCache) {
return Promise.resolve(characteristicCache);
}
log('Connecting to GATT server...');
return device.gatt.connect().
then(server => {
log('GATT server connected, getting service...');
return server.getPrimaryService(0xFFE0);
}).
then(service => {
log('Service found, getting characteristic...');
return service.getCharacteristic(0xFFE1);
}).
then(characteristic => {
log('Characteristic found');
characteristicCache = characteristic;
return characteristicCache;
});
}
// Включение получения уведомлений об изменении характеристики
function startNotifications(characteristic) {
log('Starting notifications...');
return characteristic.startNotifications().
then(() => {
log('Notifications started');
characteristic.addEventListener('characteristicvaluechanged',
handleCharacteristicValueChanged);
});
}
// Получение данных
function handleCharacteristicValueChanged(event) {
let value = new TextDecoder().decode(event.target.value);
for (let c of value) {
if (c === '\n') {
let data = readBuffer.trim();
readBuffer = '';
if (data) {
receive(data);
}
}
else {
readBuffer += c;
}
}
}
// Обработка полученных данных
function receive(data) {
log(data, 'in');
}
// Вывод в терминал
function log(data, type = '') {
terminalContainer.insertAdjacentHTML('beforeend',
'<div' + (type ? ' class="' + type + '"' : '') + '>' + data + '</div>');
}
// Отключиться от подключенного устройства
function disconnect() {
if (deviceCache) {
log('Disconnecting from "' + deviceCache.name + '" bluetooth device...');
deviceCache.removeEventListener('gattserverdisconnected',
handleDisconnection);
if (deviceCache.gatt.connected) {
deviceCache.gatt.disconnect();
log('"' + deviceCache.name + '" bluetooth device disconnected');
}
else {
log('"' + deviceCache.name +
'" bluetooth device is already disconnected');
}
}
if (characteristicCache) {
characteristicCache.removeEventListener('characteristicvaluechanged',
handleCharacteristicValueChanged);
characteristicCache = null;
}
deviceCache = null;
}
// Отправить данные подключенному устройству
function send(data) {
data = String(data);
if (!data || !characteristicCache) {
return;
}
data += '\n';
if (data.length > 20) {
let chunks = data.match(/(.|[\r\n]){1,20}/g);
writeToCharacteristic(characteristicCache, chunks[0]);
for (let i = 1; i < chunks.length; i++) {
setTimeout(() => {
writeToCharacteristic(characteristicCache, chunks[i]);
}, i * 100);
}
}
else {
writeToCharacteristic(characteristicCache, data);
}
log(data, 'out');
}
// Записать значение в характеристику
function writeToCharacteristic(characteristic, data) {
characteristic.writeValue(new TextEncoder().encode(data));
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="styles.css" rel="stylesheet">
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png">
<link rel="manifest" href="manifest.json">
<link rel="mask-icon" href="safari-pinned-tab.svg" color="#5bbad5">
<meta name="theme-color" content="#ffffff">
</head>
<body>
<button id="connect" type="button">Connect</button>
<button id="disconnect" type="button">Disconnect</button>
<div id="terminal"></div>
<form id="send-form">
<input id="input" type="text">
<button type="submit">Send</button>
</form>
<script src="main.js"></script>
<script src="companion.js" data-service-worker="sw.js"></script>
</body>
</html>