Espruino:Примеры/Использование спецификации Web Bluetooth с Espruino

Материал из Онлайн справочника
Перейти к навигацииПерейти к поиску

Перевод: Максим Кузьмин
Проверка/Оформление/Редактирование: Мякишев Е.А.


Использование спецификации Web Bluetooth с Espruino[1]

Web Bluetooth позволяет сайту напрямую подключаться к BLE-устройствам.

Кроме того, есть библиотека UART.js, содержащая API для доступа к Bluetooth- и последовательным (USB) устройствам в сети.

Примечание: Web Bluetooth в данный момент работает на Windows, MAC OS, Android, Chromebook, Linux и iOS (при помощи этого приложения).

Для работы Web Bluetooth необходимо, чтобы он был запущен на сайте на HTTPS (не HTTP). Вы можете настроить это сами при помощи Let’s Encrypt, но здесь мы этот способ описывать не будем.

Здесь мы воспользуемся GitHub Pages.

  • Залогиньтесь или создайте аккаунт в GitHub.com.
  • Кликните по вкладке Repositories, а затем на New.
  • Назовите новый репозиторий «PuckTest» и поставьте галочку рядом с пунктом Add a README file в меню Initialize this repository with. Затем кликните на кнопку Create repository (если этого не сделать, придется использовать инструменты командной строки для создания нового файла).
  • Кликните на вкладку Settings справа вверху.
  • Пролистайте вниз до меню GitHub Pages, в подменю Source выберите пункт main и нажмите на Save.
  • Вернитесь на вкладку Code, справа вверху нажмите на Add file, а затем на Create new file.
  • Назовите файл «test.html».
  • Теперь просто скопируйте и вставьте в поле на вкладке Edit new file код ниже, а затем кликните на кнопку Commit new file внизу.
<html>
 <head>
 </head>
 <body>
  <script src="https://www.puck-js.com/puck.js"></script>
  <button onclick="Puck.write('LED1.set();\n');">LED On!</button>
  <button onclick="Puck.write('LED1.reset();\n');">LED Off!</button>
 </body>
</html>

Теперь у вас должна появиться собственная страница по ссылке «https://ваш_никнейм.github.io/PuckTest/test.html».

Теперь кликните на кнопку «On!» В результате должно появиться вот такое окно:

Если вы выберете свой Puck.js и кликните на Pair, то спустя несколько секунд на Puck.js должен загореться красный светодиод. Теперь можете кликнуть на кнопку «Off!», чтобы выключить его, и он должен выключиться.

Итак, что произошло? HTML-код выше создает две кнопки, и при нажатии на них вызывается функция Puck.write(), которая есть в скрипте «puck.js», который мы загрузили выше.

Функция Puck.write() передает заданную в ней строку напрямую Puck.js в виде команды. Символ новой строки (\n) нужен, чтобы Espruino понимала, где находится конец команды, а затем выполняла ее.

Могу ли я отправлять команды на Puck.js сразу с загрузкой страницы? К сожалению, нет – в целях безопасности реализации Web Bluetooth могут подключаться к BLE-устройствам только в ответ на ввод пользовательской информации. Но уже после этого можно делать что угодно.

Это максимально простой пример, так что давайте теперь попробуем сделать что-то посложнее.

Во-первых, давайте найдем иконку лампочки. Нам подойдет SVG, т.к. картинки в этом формате хорошо выглядят даже при увеличении масштаба, и кроме того, мы можем поместить все SVG-изображение целиком в наш HTML-файл.

  • Заходим на materialdesignicons.com.
  • В поле поиска вбиваем «lightbulb».
  • Кликните на иконку с лампочкой, в появившемся окне кликните на </>, а затем на View SVG.
  • Скопируйте код, который появится в новом окне.
  • Теперь вернитесь в свой репозиторий на GitHub, кликните на файл, а потом на иконку с карандашом (Edit this file)
  • Вставьте SVG-код сразу после тега <body>.
  • Если посмотреть на веб-страницу сейчас, иконка лампочки будет маленькой. Поэтому нам надо ее растянуть, так что мы добавим немного CSS, чтобы заполнить картинкой все окно. Добавьте этот код внутрь тега <head>:
<style>
    body { margin:0;  }
    svg {
      display:block; position:absolute;
      top:0%; left:0%; width:100%; height:100%;
    }
</style>
  • И удалите из SVG фрагмент style=. Теперь ваш код должен выглядеть вот так:
<html>
 <head>
   <style>
     body { margin:0;  }
     svg {
       display:block; position:absolute;
       top:0%; left:0%; width:100%; height:100%;
     }
   </style>    
 </head>
 <body>
  <svg viewBox="0 0 24 24">
    <path fill="#000000" d="M12,2A7,7 0 0,0 5,9C5,11.38 6.19,13.47 8,14.74V17A1,1 0 0,0 9,18H15A1,1 0 0,0 16,17V14.74C17.81,13.47 19,11.38 19,9A7,7 0 0,0 12,2M9,21A1,1 0 0,0 10,22H14A1,1 0 0,0 15,21V20H9V21Z" />
  </svg>
  <script src="https://www.puck-js.com/puck.js"></script>
  <button onclick="Puck.write('LED1.set();\n');">On!</button>
  <button onclick="Puck.write('LED1.reset();\n');">Off!</button>
 </body>
</html>
  • Теперь, если перезагрузить страницу, она будет выглядеть вот так:

Примечание: Если ничего не изменилось, это могло произойти из-за того, что веб-страница закэшировалась. Если добавить в конец URL-ссылки ?1 (возможно, вместо единицы нужно будет поставить цифру побольше), это выполнит принудительную перезагрузку страницы.

Теперь можно приступить к созданию интерактивности. Мы заменим обе кнопки кликом на SVG-изображение:

  • Редактируем код, заменяя HTML для кнопок на скрипт, который будет срабатывать при нажатии на кнопку:
<html>
 <head>
   <style>
     body { margin:0;  }
     svg {
       display:block; position:absolute;
       top:0%; left:0%; width:100%; height:100%;
     }
   </style>    
 </head>
 <body>
  <svg viewBox="0 0 24 24">
    <path fill="#000000" d="M12,2A7,7 0 0,0 5,9C5,11.38 6.19,13.47 8,14.74V17A1,1 0 0,0 9,18H15A1,1 0 0,0 16,17V14.74C17.81,13.47 19,11.38 19,9A7,7 0 0,0 12,2M9,21A1,1 0 0,0 10,22H14A1,1 0 0,0 15,21V20H9V21Z" />
  </svg>
  <script src="https://www.puck-js.com/puck.js"></script>
  <script type="text/javascript">
    // Считываем кривую внутри SVG-картинки.
    // Если у вас многосоставная SVG-картинка,
    // то вы можете сделать так, чтобы разные ее элементы
    // делали разные вещи.
    var path = document.getElementsByTagName('path')[0];
    // При наведении курсора на лампочку превращаем его в руку,
    // а затем окрашиваем в серый цвет.
    path.style="cursor:pointer;fill:#BBB";
    // Теперь отправляем команды 
    // для включения и выключения светодиода.
    var on = false;
    path.addEventListener("click", function() {
      on = !on;
      if (on) {
        path.style.fill="red";
        Puck.write('LED1.set();\n');
      } else {
        path.style.fill="#444";
        Puck.write('LED1.reset();\n');
      }
    });
  </script>
  </body>
 </html>
  • Сохраняем и перезагружаем страницу. Если все было сделано правильно, теперь с ее помощью можно управлять светодиодом Puck.

А с помощью этого кода можно переключаться между разным цветами:

<html>
 <head>
   <style>
     body { margin:0;  }
     svg {
       display:block; position:absolute;
       top:0%; left:0%; width:100%; height:100%;
     }
   </style>    
 </head>
 <body>
  <svg viewBox="0 0 24 24">
    <path fill="#000000" d="M12,2A7,7 0 0,0 5,9C5,11.38 6.19,13.47 8,14.74V17A1,1 0 0,0 9,18H15A1,1 0 0,0 16,17V14.74C17.81,13.47 19,11.38 19,9A7,7 0 0,0 12,2M9,21A1,1 0 0,0 10,22H14A1,1 0 0,0 15,21V20H9V21Z" />
  </svg>
  <script src="https://www.puck-js.com/puck.js"></script>
  <script type="text/javascript">
    // Считываем кривую внутри SVG-картинки.
    // Если у вас многосоставная SVG-картинка,
    // то вы можете сделать так, чтобы разные ее элементы
    // делали разные вещи.
    var path = document.getElementsByTagName('path')[0];
    // При наведении курсора на лампочку превращаем его в руку,
    // а затем окрашиваем в серый цвет.
    path.style="cursor:pointer;fill:#BBB";
    // ниже – возможные состояния,
    // которые можно задать для светодиода:
    var state = 0;
    var states = [
      { color : "#444", command : "digitalWrite([LED3,LED2,LED1],0);\n" },
      { color : "red", command : "digitalWrite([LED3,LED2,LED1],1);\n" },
      { color : "green", command : "digitalWrite([LED3,LED2,LED1],2);\n" },
      { color : "blue", command : "digitalWrite([LED3,LED2,LED1],4);\n" },
    ];
    // Теперь отправляем команды 
    // для включения и выключения светодиода.
    path.addEventListener("click", function() {
      state++;
      if (state>=states.length)
        state=0;
      path.style.fill=states[state].color;
      Puck.write(states[state].command);
    });
  </script>
</body>
</html>

Дополнительные функции

В библиотеке Puck также есть функция Puck.setTime(optional_callback), которая задает время Puck.js на время вашего компьютера. Это может пригодиться, если работа вашего проекта зависит от времени, и вы хотите, чтобы часы Puck.js всегда были заданы на правильное время.

Считывание данных с Puck.js

В данный момент мы просто отправляем данные на Puck.js и ничего не получаем назад.

Но сделать это очень просто. Будучи подключенными к Puck.js, при помощи созданной нами веб-страницы откройте в Chrome «Инструменты разработчика» ( Ctrl + ⇧ Shift + I ,  F12  или нажав в правом верхнем углу Chrome кнопку с тремя точками, а затем «Дополнительные инструменты» > «Инструменты разработчика»).

Теперь введите следующее в поле Console:

Puck.eval("BTN.read()",function(x) { console.log(x); })

Это напечатает в консоли false. Но если нажать на Puck.js и снова ввести команду выше, это напечатает в консоли true.

То есть это выполняет команду на Puck.js (обратите внимание, что там нет символов новой строки – они не нужны), а затем вызывает функцию обратного вызова, содержащую результат.

Хорошо, ну а почему бы просто не вызвать функцию обычным образом, чтобы она нам так же вернула результат? Дело в том, что отправка данных на Puck.js и получение ответа требует времени. Если ваш код будет ждать ответа, то вся веб-страница просто перестанет работать.

А при помощи способа выше мы задаем функцию, которая вызывается по прибытию данных, а все остальное продолжает работать в нормальном режиме.

И как это можно использовать? Мы можем модифицировать код для управления светодиодом выше таким образом, чтобы цвет иконки лампочки на веб-странице менялся в зависимости от яркости вокруг Puck.js.

Просто создаем на GitHub новый HTML-файл, как уже делали выше, и вставляем в него вот этот код:

<html>
 <head>
   <style>
     body { margin:0;  }
     svg {
       display:block; position:absolute;
       top:0%; left:0%; width:100%; height:100%;
     }
   </style>    
 </head>
 <body>
  <svg viewBox="0 0 24 24">
    <path fill="#000000" d="M12,2A7,7 0 0,0 5,9C5,11.38 6.19,13.47 8,14.74V17A1,1 0 0,0 9,18H15A1,1 0 0,0 16,17V14.74C17.81,13.47 19,11.38 19,9A7,7 0 0,0 12,2M9,21A1,1 0 0,0 10,22H14A1,1 0 0,0 15,21V20H9V21Z" />
  </svg>
  <script src="https://www.puck-js.com/puck.js"></script>
  <script type="text/javascript">
    // Считываем кривую внутри SVG-картинки.
    // Если у вас многосоставная SVG-картинка,
    // то вы можете сделать так, чтобы разные ее элементы
    // делали разные вещи.
    var path = document.getElementsByTagName('path')[0];
    // При наведении курсора на лампочку превращаем его в руку,
    // а затем окрашиваем в серый цвет.
    path.style="cursor:pointer;fill:#BBB";

    function getLightValue() {
      Puck.eval("Puck.light()", function(v) {
        path.style.fill="rgb("+Math.round(v*255)+",0,0)";
        setTimeout(function() {
          getLightValue();
        }, 250);
      });
    }

    // После клика пробуем прочесть значение яркости.
    path.addEventListener("click", function() {
      getLightValue();
    });
  </script>
 </body>
</html>

Теперь после клика на иконку лампочки веб-страница подключится к Puck.js, а сама иконка поменяет цвет в зависимости от окружающей яркости (попробуйте направить на Puck.js больше света или вообще чем-нибудь ее накрыть).

Примечание: Мы запрашиваем новые данные только после получения последней оставшейся группы данных. Это лучше, чем использовать что-то вроде setInterval(), потому что при возникновении ошибок с передачей данных может скопиться много запросов на получение данных.

Это простой и рабочий способ – правда, не особо быстрый.

Двусторонняя коммуникация

А теперь вариант чуть получше – давайте попробуем сделать управление более низкоуровневым. Мы можем настроить Espruino на автоматическую отправку данных по Bluetooth (без запроса), а также автоматическую обработку этих данных.

Сделать это можно при помощи функции Puck.connect(callback). После подключения она вызывает функцию обратного вызова callback() с объектом соединения, который затем можно использовать для отправки и получения данных.

Примечание: Вы не можете одновременно и на одном и том же соединении использовать функции Puck.connect(), Puck.write() и Puck.eval(). Если вы хотите выполнить запись в Puck.js после Puck.connect(), вам надо воспользоваться connection.write() и обрабатывать все ответы с помощью обработчика connection.on("data").

Попробуйте пример ниже:

<html>
 <head>
   <style>
     body { margin:0;  }
     svg {
       display:block; position:absolute;
       top:0%; left:0%; width:100%; height:100%;
     }
   </style>    
 </head>
 <body>
  <svg viewBox="0 0 24 24">
    <path fill="#000000" d="M12,2A7,7 0 0,0 5,9C5,11.38 6.19,13.47 8,14.74V17A1,1 0 0,0 9,18H15A1,1 0 0,0 16,17V14.74C17.81,13.47 19,11.38 19,9A7,7 0 0,0 12,2M9,21A1,1 0 0,0 10,22H14A1,1 0 0,0 15,21V20H9V21Z" />
  </svg>
  <script src="https://www.puck-js.com/puck.js"></script>
  <script type="text/javascript">
    // Считываем кривую внутри SVG-картинки.
    // Если у вас многосоставная SVG-картинка,
    // то вы можете сделать так, чтобы разные ее элементы
    // делали разные вещи.
    var path = document.getElementsByTagName('path')[0];
    // При наведении курсора на лампочку превращаем его в руку,
    // а затем окрашиваем в серый цвет.
    path.style="cursor:pointer;fill:#BBB";

    // Вызывается при получении строчки данных 
    // и обновляет цвет иконки лампочки.
    function onLine(v) {
      console.log("Получено: "+JSON.stringify(v));
      path.style.fill="rgb("+Math.round(v*255)+",0,0)";
    }

    // После клика подключаемся или отключаемся.
    var connection;
    path.addEventListener("click", function() {
      if (connection) {
        connection.close();
        connection = undefined;
      }
      Puck.connect(function(c) {
        if (!c) {
          alert("Подключиться не получилось!");
          return;
        }
        connection = c;
        // Обрабатываем полученные данные 
        // и вызываем online() при каждом получении строчки.
        var buf = "";
        connection.on("data", function(d) {
          buf += d;
          var i = buf.indexOf("\n");
          while (i>=0) {
            onLine(buf.substr(0,i));
            buf = buf.substr(i+1);
            i = buf.indexOf("\n");
          }
        });
        // Сначала выполняем сброс Puck.js.
        connection.write("reset();\n", function() {
          // Ждем, пока Puck.js выполнит сброс самой себя.
          setTimeout(function() {
            // Теперь делаем, чтобы Puck.js 10 раз в секунду
            // передавала через Bluetooth значение яркости.
            // Кроме того, после отключения выполняем сброс Puck.js, 
            // чтобы setInterval() не расходовала заряд батареи.          connection.write("setInterval(function(){Bluetooth.println(Puck.light());},100);NRF.on('disconnect', function() {reset()});\n",
              function() { console.log("Готово..."); });
          }, 1500);
        });
      });
    });
  </script>
 </body>
</html>

Этот код:

  • Выполняет подключение при нажатии на лампочку.
  • Регистрирует обработчик событий connection.on("data", ...), который будет вызывать функцию onLine() при каждом получении строчки.
  • Отправляет reset() на Puck.js – это необязательно, но эта функция выполняет программный сброс, который стирает весь ранее отправленный код.
  • Ждет выполнения программного сброса. Сам сброс выполняется быстро, но отправка текста, созданного самим сбросом, выполняется медленно.
  • Отправляет setInterval(function(){Bluetooth.println(Puck.light());},100); – этот фрагмент кода побуждает Espruino каждые 100 миллисекунд передавать по Bluetooth-связи текущее значение яркости.
  • При каждом получении строчки вызывает onLine() и обновляет цвет иконки лампочки.
  • Также отправляет NRF.on('disconnect', function() {reset()});. Это нужно для того, чтобы после отключения Puck.js выполнил сброс самого себя – благодаря этому перестанет запускаться setInterval(), а значит и расходоваться заряд батареи. Но можно поступить и более изящно, при помощи clearInterval() удалив лишь тот интервал, что был запущен лично вами.

Примечание: Мы используем не console.log(), а Bluetooth.println(), потому что запись в консоль удаляет символ приглашения (>), пишет текст, а затем возвращает символ приглашения (>). Но если мы будем делать запись напрямую в Bluetooth, консоль не будет в курсе того, что происходит, и не будет печатать никаких лишних символов.

Несколько разных соединений

Хотя в библиотеке Puck.js есть удобные функции Puck.write() и Puck.eval(), предназначенные для применения к одному Bluetooth-устройству, функция Puck.connect() позволяет создавать соединения с несколькими разными устройствами.

Количество доступных соединений зависит от используемого компьютера и того, что к нему подключено (если к нему подключена, например, Bluetooth-мышка, то одним Bluetooth-соединением у вас будет меньше), но обычно вы можете подключить около 6 устройств одновременно.

В коде ниже вам нужно будет кликнуть на кнопку Add device, чтобы добавить новое устройство. После того, как устройство будет добавлено, вы сможете при помощи нажатия на кнопку переключать светодиод LED1 на каждом устройстве по отдельности, а на правой стороне будет показана текущая температура каждого устройства.

<html>
<head>
  <script src="https://puck-js.com/puck.js"></script>
</head>
<body>
<p>Кликните на Add Device, чтобы добавить новое устройство. Добавив, вы сможете переключать LED1 при помощи нажатия на кнопку, а на правой стороне будет показана текущая температура.</p>с
<div id="devices"></div>
<button id="addDevice">Add Device</button>
<script>
var _counter = 0;

document.getElementById("addDevice").addEventListener('click', event => {
  Puck.connect(function(connection) {
    if (connection===null) {
      console.log("Соединение потеряно!");
      return;
    }

    // Считываем номер соединения, чтобы показать его на экране.
    // «conId» и «conName» - это локальные переменные
    // (вроде «connection»), так что у каждого подключенного
    // устройства будут копии этих переменных.
    _counter++;    
    var conId = _counter;
    var conName = "dev"+conId;
    // Добавляем HTML-строчку для устройства с кнопкой.
    var div = document.createElement("div");
    div.innerHTML =
      `<div>Device ${conId}: <button id="btn${conId}">LED</button>&nbsp;<span id="${conName}" style="font-family: monospace;"></span>`;
    document.getElementById("devices").append(div);
    // Выполняем сброс устройства и загружаем код, 
    // чтобы печатать температуру каждую секунду
    // (и выполнять сброс после отключения).
    connection.write("\x03\x10reset();\n", function() {
      setTimeout(function() {
        connection.write("\x10setInterval(()=>Bluetooth.println(E.getTemperature()), 1000);NRF.on('disconnect',()=>reset());\n", function() {  
          console.log(conName, "Соединение выполнено успешно");
        });
      },500);
    });
    // При нажатии на кнопку
    // отправляем команду для переключения светодиода.
    document.getElementById("btn"+conId).addEventListener('click', event => {
      console.log(conName, "Отправляем LED.toggle()");
      connection.write("\x10LED.toggle()\n");
    });
    // Обрабатываем данные, пришедшие в ответ.
    var line = "";
    connection.on('data',function(d) {
      // Этот код определяет каждую пришедшую строчку.
      line += d;
      var lines = d.split("\n");
      line = lines.pop();
      // Для каждой новой строчки.
      lines.forEach(function(l) {
        // Показываем строчку на веб-странице.
        console.log(conName, "Получили "+l);
        document.getElementById(conName).innerText = l;
      });
    });
  });
});
</script>
</body>
</html>

Панели управления

Возможно, вам также будет интересно руководство по созданию панелей управления при помощи Web Bluetooth.

См.также

Внешние ссылки