Как собирать и публиковать данные с помощью Bluetooth Low Energy с Arduino

В этом посте я иллюстрирую работу Arduino с BLE (Bluetooth Low Energy). Как обычно, поскольку моей областью является физика, я обсуждаю не общий пример (эквивалент «Привет, мир!»), а конкретное физическое приложение. Иногда, для краткости или ясности, я не буду строг, и пуристы могут воротить нос, но, как я читал в книгах А. Зи, «иногда излишняя строгость вскоре приводит к трупному окоченению >».

ВНИМАНИЕ. Я предполагаю, что вы знакомы с программированием на Arduino и C/C++. Если нет, то вы вряд ли поймете содержание этой статьи.

То, что я здесь пишу, относится ко всем видам Arduino, совместимым с протоколом BLE. Я провел тесты с Arduino MKR 1010 Wifi, измеряя давление и температуру с помощью датчика BME280.

Начнем с определения глобальных переменных и констант, и с включения библиотек (предлагаемый мной код частично извлечен из примеров, имеющихся в библиотеках для Arduino Science Journal, из которых я убрал целую серию «отвлекающих », чтобы выделить аспекты, связанные с обычной работой).

#include <ArduinoBLE.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

Adafruit_BME280 sensor; 

#define SCIENCE_KIT_UUID(val) ("555a0002-" val 
                               "-467a-9538-01f0652c74e8")

BLEService                   service(SCIENCE_KIT_UUID("0000"));
BLEFloatCharacteristic       tempChar(SCIENCE_KIT_UUID("0014"),
                                      BLENotify);
BLEFloatCharacteristic       pressChar(SCIENCE_KIT_UUID("0015"),
                                       BLENotify);
BLECharacteristic            bme280Data(SCIENCE_KIT_UUID("0020"),
                                        BLENotify, 3 * sizeof(float));

Код проще, чем кажется (в настоящее время программисты, как правило, не экономят на наборе текста с клавиатуры, как это было несколько лет назад, и это дает довольно многословный код, хотя и более понятный).

Три директивы #include<...> используются соответственно для импорта библиотек для использования BLE, общей для датчиков I2C и специальной для датчика BME280. Последний определяет класс Adafruit_BME280, из которого мы создаем экземпляр объекта, которому мы присваиваем имя sensor, представляющее наш датчик.

Директива #define SCIENCE_KIT_UUID(val) является макросом. Следующая строка описывает, как построить константу из значения, присвоенного val. Поскольку это "555a0002-" val “-467a-9538-01f0652c74e8", вызов SCIENCE-KIT_UUID("0001") фактически дает "555a0002-0001-467a-9538-01f0652c74e8", строку, определяющую макрос, в котором val заменено на 0001. Это странное значение — UUID (универсальный уникальный идентификатор). UUID, определенные Arduino Science Journal, можно увидеть в примере, предоставленном с библиотеками. Если вашему приложению не нужно взаимодействовать с другими системами, фактически вы можете выбрать UUID как случайную строку из 32 шестнадцатеричных цифр (128 бит) с соответствующими разделителями в позициях, указанных в этом примере.

Каждое приложение BLE идентифицирует устройство, предоставляющее данные, как сервис, определяемый UUID. Данные называются характеристиками, каждая из которых, в свою очередь, представлена ​​UUID.

UUID сервиса, соответствующего публикации данных для Arduino Science Journal, — 555a0002-0000-467a-9538-01f0652c74e8, который хранится в объекте service (который можно представить как переменную, если вы не знаете ООП), принадлежащем классу (процедурные программисты сказали бы «типа») BLEService.

Другими словами (используя язык, который на самом деле неверен, но может быть понят теми, кто не знаком с ООП), строка

BLEService service(SCIENCE_KIT_UUID(“0000”));

определяет переменную с именем service типа BLEService. В то же время мы присваиваем этой переменной значение, соответствующее SCIENCE-KIT_UUID("0000").

Кроме того, Arduino Science Journal ожидает получить некоторые характеристики, представляющие, среди прочего, давление (чей UUID представлен объектом pressChar, чье значение равно "555a002-0015-467a-9538-01f0652c74e8") и температуру (чей UUID равен tempChar, соответствующий "555a0002-0014-467a-9538-01f0652c74e8"). Обе эти характеристики, будучи числами с плавающей запятой, относятся к классу BLEFloatCharacteristic.

Кроме того, мы предоставляем общую характеристику, которой мы присваиваем UUID 555a002-0020-467a-9538-01f0652c74e8" в качестве объекта класса BLECharacteristic, называемого bme280Data. Эта характеристика будет использоваться для «упаковки» собранных данных в единую структуру данных. Простых характеристик, конечно, было бы достаточно, тем не менее, мы определяем их здесь избыточно, чтобы проиллюстрировать технику.

При определении характеристики UUID и свойства передаются в скобках именно в таком порядке. В случае характеристик с плавающей запятой (BLEFloatCharacteristic) единственное необходимое свойство касается метода публикации, который может быть BLERead, BLEWrite или BLENotify. Последнее гарантирует, что сервис уведомит подписавшееся на него устройство о наличии новой характеристики. Однако в случае общих характеристик в дополнение к этому свойству необходимо указать его размер, то есть пространство, необходимое для хранения значения, в байтах. В примере нам требуется, чтобы bme280Data мог хранить данные длиной, эквивалентной трем числам с плавающей запятой, которые будут: время измерения, давление и температура.

void setup() {   
  BLE.begin();   
  sensor.begin(0x76);    
  delay(2000);    
  BLE.setLocalName("ArduinoGO");   
  BLE.setDeviceName("ArduinoGO"); 
  BLE.setAdvertisedService(service);   
  service.addCharacteristic(temperatureCharacteristic);  
  service.addCharacteristic(pressureCharacteristic);
  service.addCharacteristic(bme280Data);
  BLE.addService(service);    
  BLE.advertise();   
}

setup() инициализирует связь через Bluetooth (BLE.begin()) и датчик (sensor.begin()), адрес которого, как видно из документации, равен 0x76. Подождав пару секунд для завершения операций, присваиваем сервису и устройству имя (ArduinoGO). Это имя будет видно Bluetooth-устройствам, которые на этапе поиска хотят выполнить сопряжение.

Затем мы сообщаем сервису, какие характеристики он должен опубликовать (это те, которые мы определили выше). Наконец, мы просим Arduino активировать службу с помощью BLE.addService() и сделать ее доступной для сопряжения с BLE.advertise().

bool firstConnection = true;  
void loop() {   
  while (BLE.connected()) {     
    // once a client connects stay in the loop and publish data     
    if (firstConnection) {       
      BLEDevice central = BLE.central();       
      firstConnection = false;     
    }     
    publishData();     
    delay(1000);   
  } 
}

В функции loop() мы управляем подключением внешних устройств, которые на жаргоне BLE называются Centrals (надо признать, что это не очень интуитивно понятно). Когда внешнее устройство подключается к Arduino (BLE.connected()), мы остаемся в цикле до тех пор, пока соединение не прервется. Если мы впервые входим в этот цикл (firstConnection), мы получаем данные устройства и сохраняем их в центральном объекте класса BLEDevice. В примере мы не используем центральный объект, но знать о нем может быть полезно. В цикле мы получаем и публикуем данные через функцию publishData() и повторяем эту операцию каждую секунду. publishData() определяется следующим образом.

void publishData() {
  unsigned long t0 = micros();
  float T = sensor.readTemperature();
  float p = sensor.readPressure()/1.0e2;
  unsigned long t1 = micros();
  
  // publish to BLE
  temperatureCharacteristic.writeValue(T);
  pressureCharacteristic.writeValue(p);

  float pTdata[3];
  pTdata[0] = 0.5*(t0+t1);
  pTdata[1] = p;
  pTdata[2] = T;
  bme280Data.writeValue((byte*)pTdata, sizeof(pTdata));
}

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

Мы присваиваем значения характеристикам, представляющим температуру и давление, используя метод writeValue() класса BLEFloatCharacteristic. Затем мы помещаем в три компонента массива чисел с плавающей запятой время, давление и температуру соответственно. Массив — это структура данных, состоящая из последовательности смежных байтов. Поскольку число с плавающей запятой представлено в памяти компьютера как 32-битная последовательность, на pTdata требуется 24 байта. Поэтому мы передаем адрес первого байта массива в качестве первого аргумента методу writeValue() функции bme280Data ((byte*)pTdata), который ожидает указатель на байт, а в качестве второго аргумента — длину массива, измеренную в байтах. . Имя массива pTdata представляет его указатель (т. е. адрес, по которому данные хранятся в памяти машины). Так как ptData представляет собой массив с плавающей запятой, указатель является указателем на число с плавающей запятой. Поэтому необходимо преобразовать его в указатель на байт с помощью оператора приведения, представленного (byte*).

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

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