Повстання пилососа проти кульки: як я намагався дати роботу зір, Bluetooth і трохи здорового глузду

robot vacuum ble computer vision
Oleksandr PolishchukОлександр П.
|
11 травня 2026 р.

Є проєкти, які починаються з чіткого технічного завдання, архітектурної схеми, беклогу, оцінки ризиків і кави без цукру. А є проєкти, які починаються з думки: «А що буде, якщо змусити китайський робот-пилосос ганятися за повітряною кулькою?»

Ця історія саме з другої категорії. Трохи комедійна, трохи хакерська, трохи детективна. У головних ролях: робот-пилосос, Android-додаток, Bluetooth Low Energy, декомпіляція, нативна .so бібліотека, Wireshark-настрій, стара IP-камера на скотчі та одна дуже підозріла повітряна кулька.

Спочатку я просто хотів зрозуміти, як мобільний додаток керує пилососом. Потім захотів повторити це сам. А далі, як це часто буває в програмуванні, проєкт раптово отримав сюжет, побічні квести й бос-файт із Bluetooth-пакетами.

Розділ 1. Підозрюваний: Android-додаток

Усе почалося з оригінального Android-додатка, через який робот-пилосос керувався вручну: вперед, назад, ліворуч, праворуч, стоп. Звучить просто. Але коли ти натискаєш кнопку в додатку, а десь у кімнаті оживає пластиковий диск на колесах — між цими двома подіями ховається маленька магія.

Мені стало цікаво: що саме відбувається всередині? Які команди летять по Bluetooth? Чи це звичайні байти? Чи там є шифрування? Чи є handshake? Чи робот просто вдає з себе розумного, а насправді чекає на магічне число типу 0xA5?

Тому перший крок був очевидний — декомпілювати APK.

Після декомпіляції я почав шукати частину коду, яка відповідає за Bluetooth-з’єднання. У проєкті швидко проявився знайомий слід: додаток використовував BLE-бібліотеку, схожу на Android-BLE SDK від Heaton / Aicare. Це китайська бібліотека для спрощення роботи з Bluetooth Low Energy: сканування пристроїв, підключення, читання та запис характеристик, обробка callback-ів.

На цьому етапі розслідування виглядало приблизно так:

// Умовний приклад логіки, схожої на те,
// що можна зустріти у BLE-керуванні пристроєм

bleManager.connect(device, new BleConnectCallback() {
    @Override
    public void onConnectSuccess(BleDevice bleDevice) {
        System.out.println("Пилосос на зв'язку. Підозрюваний підключений.");
    }

    @Override
    public void onConnectFail(BleDevice bleDevice, String error) {
        System.out.println("Пилосос втік у Bluetooth-туман: " + error);
    }
});

Звісно, реальний код був складнішим. Там були сервіси, характеристики, callback-и, обробка станів, різні пакети даних. Але головне стало зрозуміло: керування відбувається через BLE, а мобільний додаток просто записує певні байти у потрібну характеристику пристрою.

Розділ 2. Команди є, але вони не хочуть говорити

Наївна версія мене думала так: «Зараз знайду команду “вперед”, скопіюю байти, відправлю їх із власної програми — і пилосос поїде».

Але пилосос, виявляється, теж мав характер.

Просто надіслати один пакет було недостатньо. Перед керуванням відбувався певний обмін даними: пробудження, підключення, підготовка, можливо, внутрішня ініціалізація сесії. Деякі пакети виглядали як службові, деякі — як команди, а деякі — як щось, що спеціально придумали, щоб програміст уночі дивився на hex-дамп і ставив під сумнів свої життєві рішення.

Приклад пакета міг виглядати умовно так:

A5 5A 03 01 10 00 B9

А ти сидиш і думаєш:

  • A5 5A — це заголовок?
  • 03 — довжина?
  • 01 — тип команди?
  • 10 — рух вперед?
  • B9 — checksum?
  • чи це просто пилосос передає мені своє невдоволення?

Розділ 3. Нативна бібліотека: місце, де ховаються дракони

Частина логіки була не в Java/Kotlin-коді, а в нативній .so бібліотеці. Це вже цікавіше. Якщо Java-код після декомпіляції ще можна читати майже як книгу, то .so файл — це більше як стародавній сувій, який хтось порізав, перемішав і скомпілював із оптимізаціями.

Саме там могли ховатися речі на кшталт:

  • формування команд;
  • додавання службових байтів;
  • обчислення контрольної суми;
  • шифрування або просте обфусковане перетворення;
  • логіка handshake між додатком і пилососом.

Я почав розбирати, які функції викликаються перед відправкою команд, які байти додаються, як змінюється пакет перед записом у BLE-характеристику. Це вже було схоже не на звичайну розробку, а на технічний детектив: є пристрій, є додаток, є підозріла бібліотека, є потік байтів — і десь там правда.


// Умовний приклад того, як може виглядати
// проста підготовка пакета перед відправкою

uint8_t checksum(uint8_t* data, int length) {
    uint8_t result = 0;

    for (int i = 0; i < length; i++) {
        result ^= data[i];
    }

    return result;
}

Звісно, конкретна реалізація в реальному пристрої може бути зовсім іншою. Але загальна ідея така: пристрій рідко приймає «голу» команду. Часто треба правильно сформувати пакет: заголовок, довжина, команда, параметри, checksum, іноді ще й зашифрований блок.

Розділ 4. Пакети, перехоплення і nRF Connect

Щоб не гадати лише по декомпільованому коду, я почав дивитися на реальний обмін між додатком і пилососом. Для цього використовував кілька підходів.

robot-vacuum-ble-computer-vision

На Android дуже допомагає nRF Connect. Це зручний інструмент для роботи з BLE-пристроями: можна сканувати пристрої, дивитися сервіси, характеристики, читати значення, підписуватись на notifications та пробувати записувати дані вручну.

Типовий BLE-сценарій виглядає приблизно так:

1. Знайти пристрій
2. Підключитися
3. Отримати список сервісів
4. Знайти потрібну характеристику
5. Увімкнути notifications, якщо треба
6. Записати пакет команди
7. Подивитися відповідь пристрою

Окремо я експериментував і в терміналі на Ubuntu. Linux дає багато інструментів для роботи з Bluetooth, і хоча BLE не завжди поводиться так дружньо, як хотілося б, можливість дивитися пристрої, підключення та пробувати низькорівневу взаємодію дуже допомагає.

# Перевірити Bluetooth-контролер
bluetoothctl

# Усередині bluetoothctl
scan on
devices
connect XX:XX:XX:XX:XX:XX

У такі моменти починаєш відчувати себе не веброзробником, а людиною, яка проводить допит пилососа:

— Назвіть ваш UUID характеристики.
— ...
— Я бачу, що ви мовчите. Але ми обидва знаємо, що команда “вперед” у вас десь є.

Розділ 5. Власне ядро керування

Коли структура команд стала трохи зрозумілішою, виникла наступна ідея: зробити окреме ядро керування, яке зможе підключатися до пилососа і відправляти команди без оригінального Android-додатка.

Ідея проста: винести роботу з пристроєм у незалежний код, який потім можна буде використовувати в різних сценаріях — CLI, desktop-застосунок, інтеграція з системою комп’ютерного зору або навіть невеликий локальний сервіс.

Умовно API такого ядра може виглядати так:

class VacuumController {
public:
    bool connect(const std::string& deviceAddress);

    void moveForward();
    void moveBackward();
    void turnLeft();
    void turnRight();
    void stop();
};

А далі вже можна будувати логіку вищого рівня:

VacuumController vacuum;

if (vacuum.connect("XX:XX:XX:XX:XX:XX")) {
    vacuum.moveForward();
    std::this_thread::sleep_for(std::chrono::seconds(1));
    vacuum.stop();
}

Публічний репозиторій ядра керування можна подивитися тут: GitHub: Vacuum BLE Control Core .

Це не історія про «натиснув кнопку — усе запрацювало». Це історія про те, як команда, яка в додатку виглядає як простий tap по кнопці, насправді проходить через кілька рівнів: UI, BLE SDK, нативну бібліотеку, формування пакета, запис у характеристику, відповідь пристрою і лише потім — рух коліс.

Розділ 6. А потім я захотів дати пилососу зір

Коли з керуванням стало більш-менш зрозуміло, з’явилася інша думка: а що, якщо зробити цей пилосос трохи розумнішим?

Не в сенсі «зробити з нього конкурента Boston Dynamics». Поки що скромніше: навчити його бачити об’єкт і рухатися за ним.

Саме в цей період я активно займався комп’ютерним зором: навчав моделі, експериментував із детекцією об’єктів, відеопотоками, трекінгом, FPS, оптимізацією та різними підходами до inference. Тому ідея народилася майже сама:

Беремо стару IP-камеру, кріпимо її на пилосос скотчем, запускаємо детекцію повітряної кульки, а пилосос має їхати за нею. Що може піти не так?

Спойлер: майже все. Але саме тому це цікаво.

Розділ 7. Повітряна кулька як фінальний бос

На перший погляд задача звучить просто:

  • отримати відео з IP-камери;
  • знайти на кадрі кульку;
  • визначити, де вона відносно центру кадру;
  • дати команду пилососу повернути або їхати вперед;
  • повторювати це багато разів на секунду.

Але в реальному світі все складніше. Камера має затримку. Bluetooth має затримку. Робот має інерцію. Кулька рухається. Освітлення змінюється. Модель може помилятися. А скотч, як виявляється, не завжди є промисловим стандартом монтажу камер.

Базова логіка трекінгу може виглядати приблизно так:

if (!ballDetected) {
    vacuum.stop();
    return;
}

int frameCenterX = frameWidth / 2;
int ballCenterX = detection.x + detection.width / 2;

int offset = ballCenterX - frameCenterX;
int deadZone = 50;

if (std::abs(offset) < deadZone) {
    vacuum.moveForward();
} else if (offset < 0) {
    vacuum.turnLeft();
} else {
    vacuum.turnRight();
}

Це дуже спрощений приклад. У реальному сценарії треба враховувати:

  • стабільність детекції між кадрами;
  • фільтрацію випадкових спрацювань;
  • затримку відеопотоку;
  • обмеження частоти команд;
  • безпечну зупинку, якщо об’єкт втрачено;
  • відстань до об’єкта, якщо її можна оцінити;
  • плавне керування замість різких команд.

Розділ 8. Найбільший ворог — не модель, а затримка

У задачах комп’ютерного зору часто здається, що головна проблема — це модель. Треба краще навчити, зібрати датасет, підібрати архітектуру, підвищити точність. І це справді важливо. Але коли модель починає керувати фізичним пристроєм, у гру вступає інший ворог — latency.

Затримка може накопичуватися на кожному етапі:

  • IP-камера передає відео із затримкою;
  • програма отримує кадр не миттєво;
  • модель витрачає час на inference;
  • алгоритм приймає рішення;
  • команда йде по Bluetooth;
  • пилосос реагує не моментально.

У результаті може вийти ситуація, коли кулька вже зліва, а пилосос усе ще героїчно повертає вправо, бо саме там вона була кілька кадрів тому. Це вже не штучний інтелект, а штучна розгубленість.

Тому для таких задач важлива не лише точність моделі, а й уся система: швидкість отримання кадрів, стабільність FPS, частота команд, логіка згладжування і правильна реакція на втрату об’єкта.

Розділ 9. Що може піти не так

Якщо коротко — усе. Якщо детальніше, то ось кілька типових проблем.

1. BLE-пристрій не завжди хоче дружити

Bluetooth Low Energy — це не просто «підключився і пишеш байти». Там є сервіси, характеристики, permissions, notifications, MTU, таймінги, нестабільні підключення і поведінка, яка іноді залежить від фази місяця, версії Android і настрою пилососа.

2. Команди треба відправляти обережно

Якщо надсилати команди занадто часто, пристрій може їх ігнорувати, зависати або поводитись непередбачувано. Для керування фізичним пристроєм краще мати обмеження частоти команд і стан машини: не дублювати одну й ту саму команду без потреби.

if (nextCommand != currentCommand) {
    vacuum.send(nextCommand);
    currentCommand = nextCommand;
}

3. Детекція не дорівнює розумінню

Модель може знайти кульку на кадрі, але це ще не означає, що система «розуміє» сцену. Вона не знає, що між пилососом і кулькою може бути ніжка стола, килим, поріг або кабель зарядки, який дуже хоче стати частиною експерименту.

4. Скотч — це MVP, але не production

Камера, прикріплена скотчем, — це швидко, дешево і в дусі стартапу. Але для стабільної роботи потрібне нормальне кріплення, правильний кут огляду, живлення, кабель-менеджмент і бажано не перетворювати пилосос на кіберпанк-їжака.

Розділ 10. Навіщо це все?

Можна чесно сказати: практична користь такого експерименту не в тому, щоб отримати ідеального робота-мисливця на кульки. Практична користь — у процесі.

У цьому проєкті перетинаються одразу кілька цікавих напрямків:

  • reverse engineering Android-додатків;
  • аналіз BLE-протоколів;
  • робота з нативними бібліотеками;
  • низькорівневе формування пакетів;
  • керування фізичним пристроєм;
  • комп’ютерний зір;
  • детекція об’єктів;
  • трекінг;
  • інтеграція AI-моделі з реальним світом.

І саме такі експерименти дуже добре прокачують інженерне мислення. Бо тут недостатньо просто написати красивий код. Треба зрозуміти пристрій, протокол, обмеження заліза, поведінку мережі, затримки, помилки моделі і те, що реальний світ не має методу retry() без побічних ефектів.

Висновок: пилосос ще не Skynet, але вже підозрілий

Цей експеримент почався з цікавості: як мобільний додаток керує пилососом через Bluetooth? Потім перетворився на дослідження BLE-пакетів, декомпіляцію Android-додатка, аналіз нативної бібліотеки, спроби повторити протокол керування і побудову власного ядра для відправки команд.

А потім до історії додалася комп’ютерна зорова частина: IP-камера, детекція кульки, трекінг і бажання зробити простий китайський пилосос трохи розумнішим, ніж він планував бути із заводу.

Чи став він повноцінним автономним роботом? Ще ні. Чи навчився він дивитися на світ через камеру і потенційно ганятися за кулькою? Це вже набагато ближче до правди (Наземний роботизований комплекс :-)).

Найцікавіше в таких проєктах — не фінальний результат, а шлях. Ти починаєш із кнопки «вперед» в Android-додатку, а закінчуєш аналізом байтів, BLE-характеристик, inference-затримок і питанням: «А чи не занадто багато свободи я дав цьому пилососу?»

Але поки він ганяється лише за повітряною кулькою — людство, здається, у безпеці.