В этой статье я хочу рассказать как сделать очень простую bluetooth метеостанцию (куда уж без нее :) ) и написать мобильное приложение на Flutter для нее.



В начале рассмотрим сам сенсор


Для повторения понадобится плата Arduino nano 33 BLE sense.

Плата построена на nrf52840. Устанавливаем ее через менеджер плат в ардуино.



Сразу же установим необходимые библиотеки:


Эти библиотеки необходимы для сенсоров, которые уже распаяны на самой плате.

Немного теории, а дальше рассмотрим практическую реализацию


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

Использовался режим обычной bluetooth метки, но с модификацией ManufacturerData. Этот пакет можно передавать в каждой advertise посылке.

Общий размер advertise посылки 31 байт. Сюда входит вся необходимая информация: имя устройства, системные данные, пользовательские данные. В чистом виде пользователю в ManufacturerData остается около 20 байт. Этого хватает на передачу данных метеостанции.
Преимущества такого способа передачи данных в более низком энергопотреблении, отсутствии необходимости держать постоянный коннект с устройством, широковещательная рассылка. Такое сообщение может поймать неограниченное количество приемников в радиусе приема.
ManufacturerData устанавливается перед стартом адвертайзинга.

А теперь практическая часть


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

BLE.setLocalName("nrf52840.ru");
BLE.setConnectable(false);
byte data[8] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
BLE.setManufacturerData(data, 8);
// start advertising
BLE.advertise();

Начальные данные заданы как набор байтов, чтобы заполнить массив.

Теперь в основном коде делаем остановку advertising, останавливаем радио производим замер необходимых данных и заполняем ManufacturerData уже реальными данными, после этого запускаем вещание обратно. Эта операция производится каждые 2 сек.

BLE.stopAdvertise();
// read all the sensor values
---------------------
читаем данные с сенсоров и записываем их значения
---------------------
byte data[8] = { 0x00, 0x01, t1, t2, h1, h2, p1, p2}; // t - температура (2 байта), h - влажность (2 байта), p - давление (2 байта)
BLE.setManufacturerData(data, 8);
BLE.advertise();
// wait 2 second to print again
delay(2000);

На этом работа с датчиком закончена. Вещать датчик будет каждые 100мс и каждые 2сек обновлять данные на актуальные. Получился очень простой по коду и реализации метео сенсор.

Теперь рассмотрим мобильное приложение


Сразу сделаю оговорку: я не разработчик мобильных приложений.

Для работы я использовал VSCode с плагином Flutter. Эта среда кажется проще, чем Android Studio, как мне показалось. Для работы с BLE использовалась библиотека Flutter_blue, которая сильно упростила подключение устройства.

Логика работы приложения тоже достаточно простая. Наш сенсор вещает в режиме обычного Beacon, поэтому нужно выполнить всего пару действий:

  1. Просканировать эфир — найти устройство с заданным именем,
  2. Разобрать его ManufacturerData, чтобы отобразить данные на экране.

Давайте посмотрим как это сделано.

После старта приложения регулярно запускается таймер, который сканирует Bluetooth устройства каждые 10 сек в течение 2 сек. Чаще и дольше сканировать смысла нет, расход батареи увеличится, а датчик вещает вообще каждые 100мс.

DeviceScanner() {
   _subscribeToScanEvents();
   _timer = new Timer.periodic(const Duration(seconds: 10), startScan);
 }

void startScan(Timer timer) {
   FlutterBlue.instance.startScan(timeout: Duration(seconds: 2));
}

Дальше начинаем разбирать результаты сканирования, проверяем есть ли в списке сканирования устройство с заданным именем, и, если есть, то разбираем его посылку, и выводим результат пользователю.


  void _subscribeToScanEvents() {
    FlutterBlue.instance.scanResults.listen((scanResults) {
      for (ScanResult scanResult in scanResults) {
        if (scanResult.device.name.toString() == "nrf52840.ru") {
          final int rssi = scanResult.rssi;
          final String name = scanResult.device.name;
          final String mac = scanResult.device.id.toString();
          final double temp = scanResult.advertisementData.manufacturerData[256]
                  [0] +
              scanResult.advertisementData.manufacturerData[256][1] * 0.01;
          final double humm = scanResult.advertisementData.manufacturerData[256]
                  [2] +
              scanResult.advertisementData.manufacturerData[256][3] * 0.01;
          final double press =
              scanResult.advertisementData.manufacturerData[256][4] +
                  scanResult.advertisementData.manufacturerData[256][5] * 0.01;
          final SensorData sensorData = new SensorData(
              name: name,
              rssi: rssi,
              mac: mac,
              temperature: temp,
              humidity: humm,
              pressure: press);
          _streamController.add(sensorData);
          print(
              'Manufacturer data ${scanResult.advertisementData.manufacturerData}');
          FlutterBlue.instance.stopScan();
        }

        print(
            '${scanResult.device.name} found! mac: ${scanResult.device.id} rssi: ${scanResult.rssi}');
      }
    });
  }

Небольшой нюанс. Bluetooth библиотека для Flutter может показаться странной, данные она получает в виде массива int, а в ардуино мы формируем посылку из байтов, поэтому в сенсоре я формировал посылку чтобы упростить ее анализ. Температура 25.85 градусов разбивается на два значения 25 и 85, которые отправляются в виде отдельных byte значений, и точно так же собираются обратно.

В конечном результате получилось вот такое приложение.



Исходный код проекта можно скачать с Github.