惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Privacy International News Feed
I
Intezer
T
Tenable Blog
S
Schneier on Security
Project Zero
Project Zero
G
GRAHAM CLULEY
酷 壳 – CoolShell
酷 壳 – CoolShell
小众软件
小众软件
Know Your Adversary
Know Your Adversary
博客园 - 司徒正美
The Cloudflare Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
N
News and Events Feed by Topic
博客园 - 叶小钗
宝玉的分享
宝玉的分享
L
LINUX DO - 热门话题
aimingoo的专栏
aimingoo的专栏
S
Secure Thoughts
Forbes - Security
Forbes - Security
T
The Exploit Database - CXSecurity.com
D
Darknet – Hacking Tools, Hacker News & Cyber Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 【当耐特】
罗磊的独立博客
IT之家
IT之家
H
Hacker News: Front Page
I
InfoQ
云风的 BLOG
云风的 BLOG
S
Security Affairs
M
MIT News - Artificial intelligence
GbyAI
GbyAI
Jina AI
Jina AI
Help Net Security
Help Net Security
Engineering at Meta
Engineering at Meta
大猫的无限游戏
大猫的无限游戏
Webroot Blog
Webroot Blog
L
Lohrmann on Cybersecurity
A
About on SuperTechFans
Attack and Defense Labs
Attack and Defense Labs
The Register - Security
The Register - Security
V
V2EX
G
Google Developers Blog
D
DataBreaches.Net
Apple Machine Learning Research
Apple Machine Learning Research
C
Cybersecurity and Infrastructure Security Agency CISA
J
Java Code Geeks
W
WeLiveSecurity
Cloudbric
Cloudbric
T
Tor Project blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
BLE для Arduino: пишем команды, а не дескрипторы
AndreyRodin · 2026-05-12 · via Все публикации подряд на Хабре

Время на прочтение7 мин

Охват и читатели1.4K

Разработать Bluetooth LE устройство на Arduino не так уж сложно. А вот на стороне клиента организовать нормальный обмен командами и ответами — тут начинается настоящая боль. Хочется просто отправить команду и сразу получить ответ. Или чтобы устройство само отправляло координаты, пока едет машинка. Без погружения в GATT и без асинхронных танцев с бубном.

Я подготовил две библиотеки — для Arduino и для .NET (MAUI, WPF, WinForms), которые скрывают все сложности BLE за простым текстовым протоколом.

Возможности:

  • Текстовый протокол: удобно логировать и отлаживать.

  • Простота использования: не нужно разбираться в тонкостях GATT, характеристиках, дескрипторах, буферизации.

  • Кроссплатформенность: единый API для MAUI (Android/iOS), WPF и WinForms.

  • Надежность: автоматическая сборка фрагментированных пакетов, ожидание ответа и таймауты.

  • Расширенные сценарии: удобная работа с BLE-устройством с доступом к нативным объектам.

Код для Arduino: скетч за минуту

Загружаем библиотеку BleCommands и пишем:

#include <BLECommands.h>

BLECommandsServer server;

void setup() {
  Serial.begin(9600);
  while (!Serial);

  if (!server.begin("BLECommands Device")) {
    Serial.println("BLECommands server failed to start");
    while (true);
  }

  server
    .onCommand("PING", [](const String& command, const String& args) -> String {
      return "PONG";
    })
    .onCommand("ECHO", [](const String& command, const String& args) -> String {
      return args.isEmpty() ? command : command + " " + args;
    })
    .onCommand("GET", [](const String& command, const String& args) -> String {  
      if (args == "MAC") return BLE.address();  
      if (args == "NAME") return "BLECommands device";  
      return "WRONG ARGS";  
    });
}

void loop() {
  server.poll();
}

Первый пробел в строке — разделитель команды и аргументов. Это позволяет удобно обрабатывать такие команды, как GET MAC и GET NAME.

Можно также переопределить обработчик неизвестных команд:

server.setFallbackHandler([](const String& input) -> String {  
  return "UNKNOWN: '" + input + "'";  
});

Код для клиента C#: ещё минута

using var transport = await ArduinoClient.CreateTransportAsync(deviceName);
if (transport == null)  
{
    Console.WriteLine($"Failed to connect to device '{deviceName}'");
    return;
}

await transport.StartAsync();

var reply = await transport.SendCommandAsync("PING");
Console.WriteLine(reply); // "PONG"

Объект BleTransport содержит событие Disconnected — на случай разрыва связи.

А теперь более подробно.

Обмен текстовыми сообщениями

Когда-то, экспериментируя с Bluetooth-модулем HM-10, подключённым к Arduino через UART, я столкнулся с проблемой разрыва сообщений.  Даже короткий текст мог быть разорван: сообщение «POS 100» передавалось как две строки «POS 1» и «00». Чтобы решить эту проблему, пришлось разделять сообщения неотображаемым символом (например, ‘\n’) и собирать сообщения воедино в обработчике события обновления характеристики.

Такой же подход реализован и в библиотеке. На платах со встроенным Bluetooth проблема разрыва сообщений уже не так актуальна. Тем не менее, реализованный в библиотеке подход позволяет удобно передавать длинные строки.

Что делать, если нужно передать из устройства длинный текст, например, JSON с настройками? Библиотека Arduino автоматически разбивает текст на чанки, последовательно отправляет их и в конце отправляет финальный разделитель. На стороне клиента в дело вступает класс TokenAggregator, собирающий отдельные куски и инициирующий событие TokenReceived, когда получен разделитель.

Кроме того, можно отправлять не только ASCII, но и UTF-8. Вы можете отправлять русский текст, не заботясь о его длине, и на стороне клиента он будет правильно собран.

Запрос — ответ

В чём сложность реализации столь очевидной и знакомой парадигмы в стеке Bluetooth LE? И можно ли обойтись без неё? В принципе, если мы имеем дело с какими-нибудь датчиками — то да, можно. Тогда характеристика просто поддерживает состояние, на это и заточен GATT.

Но можно ли таким образом управлять сложным устройством? Ведь приложение должно знать, как устройство реагирует на команды. Без такой возможности реализовать сложную логику будет весьма затруднительно, а скорее — и невозможно.

Представьте, что ваше устройство — это кофемашина с 20 параметрами:

  • Температура воды, давление, помол, режим подготовки, таймеры, блокировки

  • Переходы между состояниями имеют предусловия («нельзя включить нагрев без воды»).

Если вы попытаетесь смоделировать это через одну характеристику состояния, вы столкнётесь с большими проблемами. Клиент пишет новое состояние — устройство должно проверить, возможен ли переход и ответить ошибкой, если нет. Но GATT Write не возвращает ошибку с текстом! Максимум — стандартный ATT error code без возможности передать произвольный текст ошибки. Никакого тебе «недостаточно давления в насосе».

Итак, нам нужно иметь функцию, отправляющую команду на устройство и возвращающую ответ.

Решение — две характеристики:

  • Write — для отправки команд;

  • Notify — для получения ответов.

Клиент пишет команду в характеристику со свойством Write, подписывается на Notify и ждёт ответа с таймаутом. Не дождался — null. Всё просто.

  Task<string?> SendCommandAsync(string command, CancellationToken token = default);

Есть и третья характеристика для передачи периодических сообщений от устройства. Но об этом чуть позже.

Соединение

Прежде чем обмениваться информацией, с устройством нужно соединиться. Соединение с Bluetooth LE устройством в разных ОС организовано примерно одинаково:

  • Windows: достаточно получить устройство путём вызова одного из двух статических методов BluetoothLEDevice.FromBluetoothAddressAsync() или FromIdAsync(). Правда, это не означает, что соединение установилось. Тем не менее, после этого можно сразу вытаскивать сервисы из устройства — тогда соединение точно будет установлено. Нужно отметить, что если устройство не было сопряжено и не находится в системном кэше, эти функции вернут null. То есть если устройство открывается в первый раз, связаться с ним можно только через сканирование. Есть также класс GattSession, в котором можно определить время жизни соединения. Практика показывает, что после создания сессии соединение с устройством происходит почти мгновенно.

  • В Android для установки соединения используется синхронная функция device.connectGatt(). После этого, как и в Windows, процесс соединения происходит в фоне, и пользователь может получать сведения о текущем статусе в обратных вызовах.

  • В iOS соединение устанавливается классом CBCentralManager в функции connect(). И подобно Android и Windows, процесс соединения можно отслеживать и обрабатывать ошибки соединения.

Таким образом, на всех трёх платформах соединение происходит в фоновом режиме. Поэтому функция Device.ConnectAsync() не возвращает результат, а свойство Device.IsConnected меняется динамически.

Объекты Device, Service и Characteristic

Я стремился к максимальной простоте, поэтому интерфейсы содержат только самое необходимое. Например, в ICharacteristic нет дескрипторов. Это сознательное решение. Получить доступ к нативному объекту (и через него — к дескрипторам) всегда можно.

Device порождает Services, Service порождает Characteristics. Всё это disposable-объекты. Если освобождается Device, то все дочерние объекты перестают функционировать. Чтобы избавить пользователя от необходимости освобождать все объекты в иерархии, Device и Service, как наследники интерфейса IChildDisposer, сохраняют всех своих детей, полученных пользователем. Таким образом, для освобождения системных ресурсов достаточно вызвать Device.Dispose().

Windows не содержит явного метода Disconnect. Для закрытия связи с устройством достаточно вызвать Dispose у устройства. Такой же философии придерживается и библиотека. Главное — не забыть это сделать.

Прослушивание

Не всегда нужен только запрос-ответ. Представьте: вы отправили команду START, и устройство должно начать отправку своих текущих координат каждые 100 мс. Как это сделать? Для этого служит режим Listening. Для прослушивания используется третья характеристика Notify.

Устройство, получив команду, начинает передавать клиенту сообщения при помощи метода server.send(). На стороне клиента нужно подписаться на сообщение BleTransport.ListeningTokenReceived и вызвать функцию StartListening, задав таймаут между сообщениями. Если по каким-то причинам в течение таймаута от устройства ничего не пришло, срабатывает событие BleTransport.ListeningTimeoutElapsed.

При разработке своего протокола обмена стоит предусмотреть возможность сообщения клиенту о том, что прослушивание нужно прекратить. Например, когда устройство останавливается, следует передать последним сообщением какой-нибудь признак окончания движения — END или FINISH.

В примере WpfSample  можно увидеть, как это работает на практике. В скетче Messaging команда START запускает отправку сообщений «POS <n>» от 1 до 100 с периодичностью в полсекунды. В конце отправки скетч отправляет сообщение END, что сигнализирует о конце передачи.

Характеристики

Таким образом, есть три характеристики с предопределёнными UUID:

Характеристика

UUID

Свойства

Назначение

Command

DB341FB3-8977-4C2D-AC6C-74540BD8B902

Write | WriteWithoutResponse

Команды от клиента

Response

DB341FB3-8977-4C2D-AC6C-74540BD8B903

Notify | Indicate

Ответы на команды

Listening

DB341FB3-8977-4C2D-AC6C-74540BD8B904

Notify | Indicate

Периодические данные

Идентификаторы (вместе с UUID сервиса DB341FB3-8977-4C2D-AC6C-74540BD8B901) едины для Arduino и клиента.

Заметьте: характеристики позволяют отправлять данные как с подтверждением, так и без (свойства Write | WriteWithoutResponse и Notify | Indicate). Это даёт клиенту свободу выбора. Библиотека отправляет команды на устройство с подтверждением, а для Response и Listening-характеристик используется режим Notify.

Кроссплатформенность без сюрпризов

Библиотека существует в двух версиях:

  • BleCommands.Windows — для WPF, WinForms, Console (нативные объекты WinRT).

  • BleCommands.Maui — для Android/iOS через популярный пакет Plugin.BLE.

BleCommands.Core содержит общие абстракции и базовые реализации.

Каждый Device, Service, Characteristic хранит нативный объект. При необходимости вы можете опуститься на уровень ниже и использовать всю мощь родного API.

На стороне Arduino библиотека BleCommands использует многоплатформенную библиотеку ArduinoBLE.

Расширенные варианты использования

Поскольку библиотека предоставляет доступ к нативным объектам, её использование не ограничивается только Arduino. В репозитории есть пример MauiSample — приложение, которое:

  • подключается к любому устройству с активным Advertising;

  • показывает сервисы и характеристики;

  • позволяет писать в характеристику текст и наблюдать за изменениями на Notify-характеристике.

Из двух дней разработки этого примера большая часть ушла на UI, а не на BLE-логику.

В примере используется класс BleScanner для поиска устройства по имени и соединения с ним и наследники классов Service и Characteristic.

Ссылки, чтобы не искать

NuGet-пакеты:

  • BleCommands.Core

  • BleCommands.Maui

  • BleCommands.Windows

Arduino library: BleCommands

Дисклеймер

Для разработки библиотеки Arduino я использовал ESP32, на других платах пока не тестировал. Для работы с Bluetooth используется стандартная библиотека ArduinoBLE, рассчитанная на применение со многими платами. Допускаю, что текущая реализация библиотеки BleCommands в Arduino может оказаться слишком прожорливой по памяти для некоторых плат из-за использования std::map и std::function.

В экспериментах по отсылке длинной строки мне удалось отправить клиенту строку длиной в 65520 символов. String длиннее создать не удаётся: видимо, это максимальный размер, который способен выделить realloc в String.

Не пишите BLE-велосипед.

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

Форкните, ставьте звёздочку, пишите issue и pull request.
Автор на связи.