Нехай Luckfox думає, а STM32 діє: face detection від камери до реле. Частина 10

Серія «STM32 з нуля без HAL» · Частина 10 · Місяць 3

У передній статті ми змусили NPU видавати координати обличчя в консоль. Гарно — але рядки в терміналі це не система. Система — коли хтось підходить до камери і фізично клацає реле. Саме це ми зробимо сьогодні.

Фінальний результат: обличчя в кадрі → Luckfox детектує через NPU → фото летить на сервер → DeepFace звіряє з базою → SSH команда на Luckfox → STM32 отримує on по UART → реле спрацьовує.

Розберемо усі етапи окремо.

◆ Архітектура — три вузли

Камера (MIPI)
    ↓
Luckfox Pico Pro (192.168.1.125)
    NPU: RetinaFace детектує обличчя
    curl: відправляє ROI фото на сервер
    UART1 (/dev/ttyS1): отримує команди від сервера через STM32
    ↓ ethernet
PC сервер (192.168.1.121)
    Flask: приймає фото
    DeepFace: звіряє з базою known_faces/
    SSH: якщо OK → команда на Luckfox → stm32_controller.py
    ↓ SSH → UART
STM32 Blue Pill
    Отримує on/off по UART
    Керує реле через SN74HC595N

Чому саме так розподілені ролі:

Luckfox детектує — NPU апаратно, швидко, в реальному часі і CPU на це не витрачається.

Сервер розпізнає — база облич може рости, моделі змінюватись, поріг підлаштовуватись. Все це без перезбірки прошивки.

STM32 діє — надійний, детермінований. Отримав команду — виконав. Без Linux overhead.

До речі, поки налаштовував ethernet між Luckfox і сервером — задумався: а як в реальних автономних системах з’єднують vision модулі з мозком? CAN? Automotive Ethernet? Luckfox не має WiFi — тільки ethernet і UART. Для стаціонарної системи це нормально, для мобільного робота — обмеження. Якщо є досвід з robotics або automotive — цікаво почути в коментарях.

◆ Модифікація main.cc — мінімально і чисто

Оригінальний main.cc лише виводить в консоль координати. Тому нам треба додати два блоки: зберегти ROI обличчя як JPEG і відправити на сервер через curl.

Зупинимось на ключових рішеннях.

IP сервера краще задати в аргументи, а не в коді:

// Не так:
#define SERVER_URL "http://192.168.1.121:5000/face"  // перезбірка при зміні IP

// А так — п'ятий аргумент командного рядка:
const char *server_url = argv[4];

Міняєш IP — просто інший аргумент при запуску. Без make.

Throttling — не більше одного запиту на 5 секунд:

static time_t last_send = 0;
time_t now = time(NULL);
if (now - last_send >= 5) {
    cv::imwrite("/tmp/face.jpg", face_img);
    // curl POST...
    last_send = now;
}

Без throttling — при кожному кадрі де є обличчя буде летіти запит. Це 10-15 POST на секунду, не порядок.

Відправка — асинхронно:

char cmd[512];
snprintf(cmd, sizeof(cmd),
    "curl -s -X POST %s "
    "-F \"image=@/tmp/face.jpg\" "
    "-F \"norm=%.3f\" "
    "-o /tmp/face_result.txt &",  // & = у фон
    server_url, norm);
system(cmd);

& в кінці — curl іде у фоновий процес. Детекція не зупиняється поки curl чекає відповідь.

system(curl ...) — не найелегантніше рішення з точки зору C++. Правильніше використовувати libcurl API напряму. Та тут думаю це більш читабельно — кожен розуміє що відбувається без знання libcurl.

Збірка — підводний камінь з file(GLOB):

CMakeLists використовує file(GLOB SRC_FILES "${SRC_DIR}/*.cc") — бере всі .cc файли в директорії. Якщо перейменував старий main.cc наприклад на mainOld.cc — CMake підхопить обидва файли і отримаєш конфлікт символів:

make[2]: *** Error 1   ← конфлікт main() з двох файлів

Рішення: видаляти старий файл, не перейменовуватиі взагалі так вже давно нічто не робить, окрім мене)).

◆ Сервер — три варіанти і що з них вийшло

Для розпізнавання облич по базі є кілька Python бібліотек. Спробував усі три — ось чесний результат.

Варіант 1 — face_recognition + dlib

Найпопулярніший на GitHub, найбільше туторіалів.

pip install face_recognition

ERROR: Failed building wheel for dlib
subprocess.CalledProcessError: Command '['cmake', '--build', ...]' returned non-zero exit status 2

dlib — це C++ бібліотека яка компілюється при встановленні. Потребує:

sudo apt install cmake libopenblas-dev liblapack-dev libx11-dev
pip install dlib
pip install face_recognition

Якщо і після цього не збирається — залежить від версії gcc і cmake. На Ubuntu 22.04 зазвичай проходить після встановлення залежностей. В мене з першого разу не пішла, тому пішов далі.

Варіант 2 — DeepFace + TensorFlow

pip install deepface

Встановлюється без компіляції. Під капотом може використовувати VGG-Face, FaceNet, ArcFace — вибираєш модель. Хто забув, ми використовуємо FaceNet + cosine similarity.

ERROR: ModuleNotFoundError: No module named 'tf_keras'

⚠ TensorFlow 2.21 відокремив Keras в окремий пакет. deepface ще не оновив залежності автоматично:

pip install tf-keras

Після цього запускається. Перший виклик DeepFace.verify() автоматично скачує модель (~92MB FaceNet weights) в ~/.deepface/weights/. Це один раз.

⚠ Перший POST запит може прийти поки модель ще качається. Flask обробляє його в окремому потоці — система не падає, але перший результат може бути повільним.

Варіант 3 — cosine similarity на numpy

Найлегший підхід — без важких бібліотек взагалі. До того ж, FaceNet вже рахує 128-мірний embedding на Luckfox. Якщо відправляти цей вектор на сервер замість фото — порівняння це буквально 5 рядків numpy:

import numpy as np

def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

Потребує модифікації main.cc щоб відправляв вектор out_fp32[128] разом з фото. Я пішов далі, але якщо хочеш найлегше рішення без TensorFlow, тоді бери це.

Я обрав Варіант 2 — DeepFace встановлюється, працює одразу, база облич — просто папка з фото, ну і оскільки я трохи грався з tensorflow.js і продовжую це робити з tensorflow і TensorFlow Lite, то для мене вибір очевидний)).

◆ Enrollment — як додати себе в базу

Найелегантніший момент системи.

Щоб сервер тебе впізнав — потрібне еталонне фото. Де його взяти? Я просто підійшов до камери. Система сфоткала моє обличчя і зберегла в received/. Я взяв це фото і поклав в базу:

cp received/face_20260521_103524_UNKNOWN.jpg known_faces/Alex.jpg

Перезавантажую базу без рестарту сервера:

curl -X POST http://localhost:5000/reload

Наступний раз — OK:Alex. Enrollment через саму систему.

Для кількох людей — та сама схема. Кожен підходить до камери, фото падає в received/, перекладаєш в known_faces/Ім'я.jpg, робиш /reload.

◆ SSH ключі на Buildroot — чому не спрацювало

Щоб сервер міг по SSH заходити на Luckfox без пароля — треба SSH ключ. Генеруємо і копіюємо:

ssh-keygen -t rsa -f ~/.ssh/luckfox -N ""
ssh-copy-id -o PubkeyAuthentication=no -i ~/.ssh/luckfox.pub [email protected]

Перевіряємо:

ssh -i ~/.ssh/luckfox -o IdentitiesOnly=yes [email protected] "echo test"

[email protected]'s password:   ← питає пароль — ключ не спрацював

Дивимось на платі:

ls -la /root/.ssh/
ls -la / | grep root

drwx------ 2 root  root   .ssh/           ← права правильні: 700
-rw------- 1 root  root   authorized_keys ← права правильні: 600
drwx------ 4 1000  1000   /root           ← а ось це проблема!

/root директорія належить uid 1000 а не root (uid 0). Buildroot створює її під іншим користувачем. SSH демон перевіряє власника home директорії — і якщо вона не належить користувачу який логіниться, відхиляє ключ з міркувань безпеки. Навіть якщо authorized_keys має правильні права.

Виправити можна через chown root:root /root — але на Buildroot це не персистентно між перезавантаженнями якщо /root монтується з tmpfs.

Практичне рішення — sshpass:

sudo apt install sshpass

sshpass -pluckfox ssh \
    -o StrictHostKeyChecking=no \
    -o PubkeyAuthentication=no \
    [email protected] "echo test"

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

◆ Реле спрацьовує

Сервер отримує фото → DeepFace порівнює з базою:

  Alex: dist=0.073 verified=True
[20260521_113501] norm(rknn)=1.442 | dist(deepface)=0.073 | OK:Alex
  [реле] відкриваємо для Alex на 5с
  [STM32] on → ok

dist=0.073 при порозі 0.40 — DeepFace дуже впевнений. Два числа в логу:

  • norm від RKNN на Luckfox — детекція на NPU
  • dist від DeepFace на PC — розпізнавання по базі

Захист від паралельних викликів:

Luckfox відправляє фото раз на 5 секунд. Але Flask обробляє запити в кількох потоках — без захисту реле спрацьовує кілька разів підряд:

relay_lock   = threading.Lock()
relay_active = False

def relay_open(name):
    global relay_active
    with relay_lock:
        if relay_active:
            print("  [реле] вже активне — пропускаємо")
            return
        relay_active = True
    # ... on → sleep → off → relay_active = False

Тепер в логах:

[реле] відкриваємо для Alex на 5с   ← перше спрацювання
[реле] вже активне — пропускаємо    ← паралельні запити ігноруються
[реле] вже активне — пропускаємо
[STM32] on → ok
[STM32] off → ok
[реле] закрито                       ← готове до наступного

◆ Клонування плати

Питання яке виникає коли система працює: як перенести на іншу плату?

update.img в ~/luckfox-pico/output/image/ — це Buildroot образ. Він містить все що зібрано через make: ядро, rootfs, наші пакети (go-dashboard, статичний IP). Прошивається одною командою:

./tools/linux/Linux_Upgrade_Tool/upgrade_tool uf output/image/update.img

⚠ Але файли які клали через scp — бінарник детекції, моделі, stm32_controller.py — в образі не збережені. Вони живуть в /root/ який не входить в rootfs образу.

Після прошивки нової плати треба скопіювати:

scp -o PubkeyAuthentication=no -r \
    luckfox_pico_rknn_example/install/uclibc/luckfox_pico_retinaface_facenet_demo \
    root@NEW_IP:/root/

scp -o PubkeyAuthentication=no \
    stm32_controller.py root@NEW_IP:/root/

Щоб файли були в образі автоматично — треба Buildroot overlay або пакет.

◆ Гайд-шпаргалка

Одноразово — збірка бінарника на PC:

export LUCKFOX_SDK_PATH=/home/alex/luckfox-pico
cd luckfox_pico_rknn_example
./build.sh
# вибираємо: 1 (uclibc) → 1 (retinaface_facenet)

scp -o PubkeyAuthentication=no -r \
    install/uclibc/luckfox_pico_retinaface_facenet_demo \
    [email protected]:/root/

scp -o PubkeyAuthentication=no \
    stm32_controller.py [email protected]:/root/

Кожного разу — запуск:

# Термінал 1: сервер на PC
cd ~/Sasha/www/STM32/python
python3 face_server_deepface.py

# Термінал 2: детекція на платі
ssh -o PubkeyAuthentication=no [email protected]

RkLunch-stop.sh
cd /root/luckfox_pico_retinaface_facenet_demo
./luckfox_pico_retinaface_facenet \
    model/RetinaFace.rknn \
    model/mobilefacenet.rknn \
    model/test.jpg \
    http://192.168.1.121:5000/face

Додати людину в базу:

# Підійди до камери → фото в received/
cp received/face_TIMESTAMP_UNKNOWN.jpg known_faces/Maria.jpg
curl -X POST http://localhost:5000/reload

Перевірити стан:

curl http://192.168.1.121:5000/status

Перевірити STM32 окремо:

# На платі
STM32_PORT=/dev/ttyS1 python3 /root/stm32_controller.py on
STM32_PORT=/dev/ttyS1 python3 /root/stm32_controller.py off

◆ Що далі

Десять статей позаду — Luckfox вже мабуть набрид, стало нудно і це момент коли треба зупинитись і подивитись з висоти. Так, а що вже є?

Місяць 1: писали STM32 регістрами без HAL, побачили що під капотом Arduino-стилю.

Місяць 2: побудували перший міст від bare-metal до Linux і там STM32 вже шле дані через UART у Buildroot-зібраний Luckfox.

Місяць 3, який закриває ця стаття: Luckfox став мозком — камера, NPU, розпізнавання облич, а STM32 виконавчий орган з реле.

Що ж я хотів зробити? Якщо взяти всі десять статей разом, то це повинен бути не випадковий набір туторіалів, а одна цікава історія: як просто зробити так, щоб маленька залізячка бачила, думала і діяла.

Але залишилось невирішеним питання яке досі обходив стороною: а що там всередині самого Linux? Ми писали userspace код, який звертався до /dev/ttyS1, /dev/video0, до драйверів які хтось колись написав. Я туди не лазив. Але думаю вже час залізти і туди.

Місяць 4 — Linux Kernel Modules на Raspberry Pi

З наступної статті будемо разом знайомитись з kernel space. Кажуть це окремий світ зі своїми правилами: немає printf, є printk; немає malloc, є kmalloc; помилився з вказівником — не segfault, а kernel panic і reboot. Кажуть, embedded middle від junior відрізняється саме цим — здатністю писати свій драйвер, а не тільки використовувати чужі.

Приблизний план на місяць, але це не точно:

  • Стаття 11 (наступна): Hello world kernel module. Перший .ko файл, insmod, dmesg, перші ґулі від нового ядра 6.12 яке вже не має старого пакета raspberrypi-kernel-headers
  • Стаття 12: GPIO LED driver з ядра. Перша різниця між sysfs способом (як ми звикли з userspace) і власним driver-ом
  • Стаття 13: character device driver — /dev/mydev з повним file_operations, як це зроблено в усіх «справжніх» драйверах
  • Стаття 14: HC-SR04 ultrasonic sensor driver — cat /dev/hcsr04127cm. Interrupt handling, тут вже не іграшки

Платформа — Raspberry Pi 3 Model B. Pi 3B+ резервую для Місяця 5 (FreeRTOS + OpenCV), решта йде в експерименти, візьму поки що звідси.

Паралельний маршрут — Zephyr на Pi Pico

Поки ця серія йде по основному на рейках «STM32 + Linux», та в мене є ще — «Збираємо комп’ютер як Neotron, але по-своєму». Там Zephyr RTOS на Pi Pico, це трохи інший спосіб думати про embedded: там не Linux ядро з його MMU і userspace, а mainline RTOS з Device Tree, kbuild і дисципліною mainline-проєктів. Хочу щоб ці дві серії доповнювали одна одну — тут ми дивимось як ядро живе, там як жити без ядра.

Відкриті питання

  • Чи варто переносити написані kernel modules з Pi на Luckfox? Інша архітектура (Cortex-A7 vs A53), Buildroot замість Debian, uclibc замість glibc. Підозрюю це буде боляче і точно буде окрема ґуля
  • STM32 ↔ Pi через SPI як kernel driver — /dev/stm32sensor з боку ядра, мій HAL з боку залізки. Якщо встигну в Місяць 4 — буде окрема стаття.

Вже не встигну бо треную Катю — wake-word модель на 50KB з AUC 0.98, повністю своя CNN. Українське слово в терміналі реагує в real-time:

). Як робив напишу в себе в блозі, можливо також і на DOU

І питання яке хочеться поставити вам: коли в роботі реально дійшло до написання свого kernel module, а не використання чужого? Це вже той досвід що відрізняє читачів які пишуть в коментарях «та я це вже років п’ять», від тих хто чесно каже «ніколи не доводилось». Пишіть, цікаво подивитись пропорцію.

До зустрічі за тиждень — Hello, kernel!

Репозиторій: github.com/pipicosim800-maker/stm32F103

◆ P.S. SD карта, swap і додаткове сховище

Поки налаштовував систему — знайшов вільний SanDisk Ultra 32GB. Вставив в Luckfox і прийшла ідея: система працює, фото летять в received/ на PC — але що якщо перенести сховище прямо на плату?

Але спочатку тре вирішити важливіше питання — пам’ять.

Скільки RAM на RV1106:

free -h

Mem:   53.3M   23.8M   10.3M
Swap:      0       0       0

53MB RAM і нуль swap. RetinaFace все одно запускається — він відносно легкий. Але якщо захочу YOLOv8 або важчі моделі:

Can't allocate memory — класична помилка RKNN при нестачі RAM. Автор статті на Habr про YOLOv8 на Luckfox Pico Mini зіткнувся з тим самим. Рішення — swap.

Перевіряємо SD карту:

ls /dev/mmcblk*
df -h | grep mmcblk

/dev/mmcblk1p1   14.1G   1.6G   12.3G  12%  /mnt/sdcard

Карта змонтована як /mnt/sdcard. Замість перерозбивки — простіший шлях через swap файл:

dd if=/dev/zero of=/mnt/sdcard/swapfile bs=1M count=512
mkswap /mnt/sdcard/swapfile
chmod 600 /mnt/sdcard/swapfile
swapon /mnt/sdcard/swapfile
free -h

Mem:    53.3M   24.3M    2.1M
Swap:  512.0M       0  512.0M

512MB swap — YOLOv8 тепер запуститься.

Автозапуск після перезавантаження:

cat > /etc/init.d/S90autoswap << 'EOF'
#!/bin/sh
case $1 in
    start)
        swapon /mnt/sdcard/swapfile
        ;;
    stop)
        swapoff /mnt/sdcard/swapfile
        ;;
    *)
        exit 1
        ;;
esac
EOF
chmod +x /etc/init.d/S90autoswap

Що це дає, наприклад для Автобота:

SD карта на мобільній платформі це зручно:

  • received/ з фото облич — прямо на карті, не на PC
  • логи детекції — зберігаємо локально
  • embeddings бази — переносити карту замість SSH
# Переносимо received/ на SD
mkdir -p /mnt/sdcard/received
# В face_server_deepface.py:
# SAVE_DIR = "/mnt/sdcard/received"  ← якщо сервер на платі

Виймаєш карту і переносиш дані на PC фізично. Без мережі, без SSH. Гадаю для польових умов використання мого Автобота це саме те.

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось12
До обраногоВ обраному6
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Гарна серія, респект
Часто запитують «з чого почати вхід в ембедед», буду тепер всім давати посилання сюди )

а чому не на stm32 напряму, є ж ті які з NPU huggingface.co/STMicroelectronics

Дякую, не знав що таке є! STM32N6 з Neural-ART — це справді окремий клас, 600 GOPS прямо на MCU. NPU на Luckfox відкрив для себе випадково, а тут цілий чіп заточений під це. Але $100+ проти $14 за Luckfox — для хобі-проектів різниця відчутна. Цікавий напрямок для окремого експерименту, можливо в майбутньому. Дякую що звернули увагу!

хоч це і не у топік, але на векторних інструкціях esp32 вже давно можна yolo та face recognition запускати, є навіть готовий набір з камерою і екранчиком щоб побудувати смарт замок github.com/...​sp32_cam_face_recognition

Чому не у топік? Саме сюди, хоч це esp32, але ж корисна інформація, яка буде в одному місці. Дякую

Підписатись на коментарі