Совсем недавно на WWDC2024 Apple представила Embedded Swift. По словам разработчиков данное нововведение поможет нам писать программы для Hardware устройств на "Pure Swift". (Раньше для таких извращений мы использовали SwiftIO)
Посмотрим, как в дальнейшем будет развиваться данная технология, эта статья совсем о другом. Я предлагаю вам окунуться немного в другую тему, которая, на мой взгляд, более полезная и универсальная - управление микроконтроллером с вашего iPhone/Mac/iPad и даже Watch посредством BLE и Apple's Core Bluetooth.
Статья разделена на 2 части: первая посвящена программированию микроконтроллера ESP, а вторая - написанию менеджера с использованием Core Bluetooth.
После прочтения данной статьи вы будете иметь представление о том, как с помощью вашего iPhone можно покорять harware-миры.
Спойлер! Дальше не будет жёсткой матчасти / документалки и пр., я постараюсь человеческим языком, с картинками передать идею: как всё работает и что вообще происходит.
О чём данная часть?
Рассмотрим на примерах работу Arduino ESP Core, а именно:
Создадим собственный сервис на плате ESP32 и подключимся к нему с нашего iPhone.
Отправим наши первые команды с iPhone на плату ESP32.
База
Ещё со школьной скамьи вы знаете, что для приёма и передачи информации нам необходимо какое-то устройство. Например, у человека устройством для приёма информации служат глаза и уши, для передачи - речевой аппарат или письмо.
У микроконтроллеров с этим дела обстоят поинтереснее, для этого им достаточно всего одного модуля. На самом деле их несколько, но сегодня мы поговорим именно о Bluetooth.
Bluetooth придумали ещё в далёком 1998 году как простой беспроводной дата-обменник и с тех пор он перетерпел довольно большое количество обновлений и изменений.
Одним из таких изменений (Bluetooth 4.0) - это выпущенная в 2009 году новая версия спецификации ядра Bluetooth Low Energy, которую apple сразу же внедрила в iPhone 4S (iPhone 4, кстати, всё ещё имел старый Bluetooth 2.1).
Ключевая особенность BLE в том, что он потребляет меньше энергии* по сравнению с классическим Bluetooth, но, к сожалению, они несовместимы.
*Это возможно благодаря глубокой оптимизации протокола, выключению передатчика при первой возможности и пересылке малых объёмов данных на низкой скорости.
Приступим к более интересной части.
Работа с микроконтроллером на базе ESP32. Создание собственного сервиса.
Далее в статье мы будем оперировать несколькими терминами, которые нам, как разработчикам полезно знать (Следующие определения будут даны в контексте Bluetooth): Server, Service и Characteristic.
Server используется ESP32 для того, чтобы размещать на нём сервисы. Мы так же будем использовать его для обработки событий подключения и отключения устройств.
Для лучшего понимания Service и Characteristic приведу аналогию с уже давно знакомой вам структурой данных — Class.
Чтобы структурировать и передавать данные, Bluetooth использует так называемые сервисы и характеристики:
Это максимально упрощённая схема:
Сервис обязательно должен хранить свой уникальный номер - мы будем использовать 128-битный UUID. Как и класс, сервис содержит в себе свойства — характеристики.
Каждая характеристика, в свою очередь, обязательно хранит свой уникальный номер (мы также будем использовать 128-битный идентификатор), а так же свойства (Read, Write, WriteNR) и своё значение.
Отправляя сообщение с iPhone на ESP32, мы записываем его в значение характеристики выбранного сервиса и плата тут же его получает.
Всё просто!
Существует 2 человеческих способа написания собственного BLE сервиса на ESP:
Используя родной фреймворк ESP-IDF.
Используя ESP Arduino Core, некой обёртки над ESP-IDF.
Второй вариант намного проще и удобнее. Его мы и рассмотрим.
Создание сервиса. ESP Arduino Core.
Для работы нам понадобится установить Arduino IDE и Arduino-ESP32 support.
Одним из преимуществ ESP Arduino Core является возможность использования языка C++. Дело в том, что сам ESP-IDF написан на чистом C, и данная обёртка позволяет экономить большое количество времени и строк кода, необходимых для создания Bluetooth сервиса.
Перед тем, как приступить к написанию сервиса, нам нужно понять, как именно мы будем принимать и обрабатывать полученные значения?
Для этой задачи существуют функции обратного вызова (Callbacks), которые используются для обработки различных событий, возникающих в процессе работы Bluetooth.
Запустите Arduino IDE и создайте новый проект.
Сначала напишем класс MyServerCallbacks
и унаследуем его от BLEServerCallbacks
, теперь в новом классе нам доступны методы для переопределения, которые служат для обработки событий подключения устройства к нашему серверу и отключения от него.
#include <BLEServer.h>
// Создание Callback функций для сервера.
class MyServerCallbacks: public BLEServerCallbacks {
public:
void onConnect(BLEServer* pServer) {
Serial.print("iDevice was successfully connected\n");
// Handle your logic on connect
}
void onDisconnect(BLEServer* pServer) {
Serial.print("iDevice was successfully disconnected\n");
// Handle your logic on disconnect
pServer->startAdvertising(); // !!!
}
};
Обратите внимание на строку №17: При отключении устройства от нашей платы, сервер нужно перезапустить, иначе вы не сможете подключиться к нему повторно. Придётся делать hard reset на плате.
Теперь напишем callback для обработки сообщений, поступающих от устройства-издателя:
class CharacteristicCallbacks: public BLECharacteristicCallbacks {
public:
void onWrite(BLECharacteristic *pCharacteristic) {
// Данный блок кода будет выполняться каждый раз, когда мы отправляем сообщение с iPhone на ESP
std::string value = pCharacteristic->getValue();
Serial.print("Message received: " + String(value.c_str()) + "\n");
}
};
Для каждой характеристики нужно создавать свой класс и унаследовать его от BLECharacteristicCallbacks
.
Метод void onWrite(BLECharacteristic *pCharacteristic)
, служит коллбэком при записи значения в характеристику. Мы можем получить это значение, вызвав getValue()
, у этой характеристики (строчка 6).
Теперь можем приступать к написанию сервиса. Разобьём нашу задачу на 5 шагов:
Создать девайс - чтобы наша плата отображалась как устройство, нужно дать ей имя в сети и проинициализировать.
Создать сервер - именно на нём будет размещён наш сервис и именно к нему будут подключаться другие устройства.
Создать сервис - на одном сервере может быть размещено несколько сервисов, под каждую задачу, но нам для примера хватит одного.
Создать характеристики - у одного сервиса может быть несколько характеристик, нам так же хватит одной для примера.
Настроить Server's Advertising - публичная информация сервера: Имя девайса, UUID и т д.
Создадим функцию
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <Arduino.h>
#define SERVICE_UUID "9A8CA9EF-E43F-4157-9FEE-C37A3D7DC12D" // ID сервиса
#define SERVICE_CHARACTERISTIC_UUID "CC46B944-003E-42B6-B836-C4246B8F19A0" // ID характеристики
void setupBLEServer() {
/* 1 */
const String devName = "ESP32_BLE_Server_TEST"; // Имя, нашей платы в списке Bluetooth устройств
BLEDevice::init(devName.c_str()); // Инициализация девайса
/* 2 */
BLEServer *pServer = BLEDevice::createServer(); // Создание сервера
pServer->setCallbacks(new MyServerCallbacks()); // Подключение Callback'а
/* 3 */
BLEService *pService = pServer->createService(SERVICE_UUID); // Cоздание сервиса
/* 4 */
BLECharacteristic *pCharacteristic; // Инициализирую характеристику для передачи сообщений
pCharacteristic = pService->createCharacteristic(SERVICE_CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR);
pCharacteristic->setCallbacks(new CharacteristicCallbacks());
pService->start();
/* 5 */
BLEAdvertising *pAdvertising = pServer->getAdvertising();
BLEAdvertisementData adv;
adv.setName(devName.c_str());
adv.setCompleteServices(BLEUUID(SERVICE_UUID));
pAdvertising->setAdvertisementData(adv);
pAdvertising->setScanResponseData(adv);
pAdvertising->start();
}
Пояснения:
Выбираете любое имя для вашей платы, можете использовать также её Chip ID:
String((uint32_t)(ESP.getEfuseMac() >> 24), HEX)
После создания сервера сразу подключаем к нему заранее написанный класс
MyServerCallbacks
При создании сервиса, в инициализатор передаём SERVICE_UUID — 128-ти битный идентификатор. Можете использовать абсолютно любую последовательность символов.
-
Здесь бы хотелось остановиться поподробнее: При создании характеристики, в инициализатор передаём сначала её UUID, затем свойства:
BLECharacteristic::PROPERTY_READ
- Если характеристика имеет это свойство, клиенты (например, смартфоны или другие BLE-устройства) могут запрашивать текущее значение характеристики и получать его в ответ.BLECharacteristic::PROPERTY_WRITE
- Если характеристика имеет это свойство, клиенты могут отправлять данные для записи в характеристику.BLECharacteristic::PROPERTY_WRITE_NR
- Если характеристика имеет это свойство, клиенты могут отправлять данные для записи в характеристику, но не будут получать подтверждение от сервера. Это может быть полезно для ускорения передачи данных, когда подтверждение не требуется.
Поскольку мы будем только отправлять сообщения с iPhone на ESP32, нам не нужны свойства READ и WRITE, но я оставлю их для примера.
Класс
BLEAdvertising
используется для хранения публичной информации о сервере, не будем вникать в детали, нам достаточно знать, что через него мы устанавливаем отображаемое имя и UUID сервера, дальше я покажу как это будет выглядеть
Теперь всё, что нам осталось, это вызвать нашу функцию в методе setup()
void setup() {
// ...
setupBLEServer();
}
Давайте посмотрим на весь код целиком:
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <Arduino.h>
#define SERVICE_UUID "9A8CA9EF-E43F-4157-9FEE-C37A3D7DC12D" // ID сервиса
#define SERVICE_CHARACTERISTIC_UUID "CC46B944-003E-42B6-B836-C4246B8F19A0" // ID характеристики
// Создание Callback функции для сервера.
class MyServerCallbacks: public BLEServerCallbacks {
public:
void onConnect(BLEServer* pServer) {
Serial.print("iDevice was successfully connected\n");
// Handle your logic on connect
}
void onDisconnect(BLEServer* pServer) {
Serial.print("iDevice was successfully disconnected\n");
// Handle your logic on disconnect
pServer->startAdvertising(); // !!!
}
};
class CharacteristicCallbacks: public BLECharacteristicCallbacks {
public:
void onWrite(BLECharacteristic *pCharacteristic) {
// Данный блок кода будет выполняться каждый раз, когда мы отправляем сообщение с iPhone на ESP
std::string value = pCharacteristic->getValue();
Serial.print("Message received: " + String(value.c_str()) + "\n");
}
};
void setupBLEServer() {
/* 1 */
const String devName = "ESP32_BLE_Server_TEST"; // Имя, нашей платы в списке Bluetooth устройств //Можете использовать mac address вашей платы: String((uint32_t)(ESP.getEfuseMac() >> 24), HEX)
BLEDevice::init(devName.c_str()); // Инициализация девайса
/* 2 */
BLEServer *pServer = BLEDevice::createServer(); // Создание сервера
pServer->setCallbacks(new MyServerCallbacks()); // Подключение Callback-а
/* 3 */
BLEService *pService = pServer->createService(SERVICE_UUID); // Cоздание сервиса
/* 4 */
BLECharacteristic *pCharacteristic; // Инициализирую характеристику для передачи сообщений
pCharacteristic = pService->createCharacteristic(SERVICE_CHARACTERISTIC_UUID, /*BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE |*/BLECharacteristic::PROPERTY_WRITE_NR);
pCharacteristic->setCallbacks(new CharacteristicCallbacks());
pService->start();
/* 5 */
BLEAdvertising *pAdvertising = pServer->getAdvertising();
BLEAdvertisementData adv;
adv.setName(devName.c_str());
adv.setCompleteServices(BLEUUID(SERVICE_UUID));
pAdvertising->setAdvertisementData(adv);
pAdvertising->setScanResponseData(adv);
pAdvertising->start();
}
void setup() {
Serial.begin(9600);
setupBLEServer();
}
void loop() {}
Вот и всё! Благодаря ESP Arduino Core мы создали полноценный сервис в ~15 строчек! Давайте зальём скетч на плату и проверим, всё ли работает.
Пока для теста будем использовать любой BLE Scanner, взятый с AppStore. После того, как скетч загрузился, заходим в приложение и видим нашу плату в списке доступных устройств:
Подключившись к серверу, мы видим информацию о сервисе, его UUID совпадает с тем, что мы указали.
Зайдя в сервис, мы видим нашу характеристику, её UUID, свойства и текущее значение (которого нет). Нажав на WriteWithoutResponse мы сможем отправить первое сообщение.
Посмотрите на наши колбэки. Если вы подключитесь, отправите сообщение: "Hello, world!" и отключитесь, то лог будет примерно таким:
Вы только что подружили свой iPhone с ESP32 и на это ушло всего 80 строчек! ESP Arduino Core сильно облегчает нам жизнь в написании Bluetooth сервисов.
Вот и конец первой части.
Во второй нам предстоит написать собственный менеджер, используя Core Bluetooth API, чтобы уже окончательно подружить наш яблочный девайс с китайским маленьким монстром.