Как собирать и публиковать данные с помощью 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-устройство, оно уведомляет последнее о наличии отдельных значений температуры и давления, а также о наличии массива из трех элементов, первый из которых представляет время сбора данных, второе давление и третье температуру.
В следующем посте будет объяснено, как перехватывать эти значения и как интерпретировать их физический смысл. Быть в курсе.