Веб-камера — глаза робота: пишу веб-приложение на FastApi для управления DIY-проектом. Часть 1

7354f290681ebd960bd827aa4c8e354f.png

Эта статья открывает цикл публикаций о создании open-source веб-приложения для стриминга видео с веб-камеры и управления роботом. Приложение позволит транслировать видео с камеры в реальном времени и отправлять команды управления роботом через интерфейс. Думаю, статья будет интересна веб-программистам, интересующимся работой с видеостримингом и FastAPI, а также робототехникам и энтузиастам DIY-проектов.

Идея проекта возникла из моего интереса к робототехнике и веб-программированию. Ранее в статье DIY-проект: гусеничная платформа с ИК-управлением на Arduino я создал гусеничную платформу на базе Iscra mini, управляемую ИК-пультом, и захотел развить эту платформу.

В качестве камеры я планирую использовать экшн-камеру, которая может работать как веб-камера. Если она окажется несовместимой с Linux, её можно будет заменить обычной веб-камерой. Основная цель проекта — создать гибкое решение, которое будет полезным для разных DIY-проектов.

Стриминг видео с веб-камеры

Я начал эксперименты со стримингом видео, используя универсальные UVC-драйверы в Linux Debian. UVC (USB Video Class) — это стандарт, разработанный USB Implementers Forum (USB-IF), который определяет, как веб-камеры, цифровые видеокамеры или другие видеоустройства должны передавать видеопоток через USB. Универсальные драйверы добавят удобства определения в системе разных веб-камер.

Проще говоря, это программное обеспечение в операционной системе (в моём случае в Linux), которое обеспечивает взаимодействие между ядром и UVC-совместимыми устройствами. В Linux этот драйвер называется uvcvideo и встроен в ядро как часть подсистемы Video4Linux2 (V4L2).

Для стриминга видео с камеры я планирую использовать программу MJPG-Streamer — готовое решение для трансляции видео по HTTP. Само веб-приложение будет подключаться к нему по URL. Использование этой программы сократит время на разработку, экономя его на механизме трансляции видеопотока.

Теперь я установлю MJPG-Streamer и все необходимые зависимости на целевое устройство. Сейчас я тестирую на ПК с Debian, однако этот процесс почти идентичен установке для Raspbian или Arbian, которые работают на платах Raspberry Pi и Orange Pi (в будущем я буду использовать такие платы для постройки робота в статьях DIY).

Шаг 1: Обновление системы

Обновляю список пакетов и устанавливаю их:

sudo apt-get update
sudo apt-get upgrade -y

Эти команды нужны для предотвращения проблем с совместимостью пакетов.

Шаг 2: Установка зависимостей

Теперь устанавливаю зависимости:

sudo apt-get install -y build-essential cmake libjpeg-dev libv4l-dev ffmpeg

Разбор команды:

  • build-essential включает gcc и make, необходимые для сборки программ:

    • gcc — компилятор для языков C/C++;

    • make — инструмент для автоматизации сборки программ;

  • libjpeg-dev — библиотека для обработки JPEG;

  • libv4l-dev — библиотека для работы с Video4Linux2 (V4L2), которая используется для обработки видео с веб-камер.

  • Cmake — это кроссплатформенная система управления сборкой, которая автоматически генерирует файлы проектов и Makefile для упрощения процесса компиляции программ;

  • ffmpeg — инструмент для записи, конвертации, обработки и потоковой передачи аудио- и видеофайлов, поддерживающий множество форматов и кодеков.

Эти зависимости помогут правильно собрать программу для видеострима MJPG-Streamer и обрабатывать видео с веб-камеры.

Для дополнительного тестирования камеры я установлю утилиту v4l-utils:

sudo apt-get install -y v4l-utils

Утилита v4l-utils позволит мне узнать информацию о режимах камеры для её настройки и проверки.

Шаг 3: Установка MJPG-Streamer

Программа MJPG-Streamer позволяет получать видеопоток с веб-камеры, обращаясь к нему через URL-адрес. Я выбрал её, так как это простое готовое решение для видеострима.

Устанавливаю программу для видеострима:

git clone https://github.com/jacksonliam/mjpg-streamer.git
cd mjpg-streamer/mjpg-streamer-experimental
make && sudo make install
export LD_LIBRARY_PATH=/usr/local/lib

Разбор команд:

  • git clone https://github.com/jacksonliam/mjpg-streamer.git — клонирование репозитория MJPG-Streamer;

  • cd mjpg-streamer/mjpg-streamer-experimental — переход в директорию для сборки;

  • make && sudo make install — компиляция и установка;

  • export LDLIBRARYPATH=/usr/local/lib — задаёт переменную окружения для зависимостей.

Шаг 4: Проверка устройства

Теперь мне нужно понять, как обращаться к веб-камере для работы с ней. Для этого я подключаю веб-камеру к устройству через USB. Устройством может быть ПК, ноутбук или плата типа Orange pi.

Проверяю наличие подключенных видеоустройств:

ls /dev/video*

У меня два виртуальных устройства, связанных с моей веб-камерой:

/dev/video0  /dev/video1

Далее я хочу проверить, какие форматы, разрешения экрана и количество fps доступны для моей камеры.

Проверяю видеоформаты:

v4l2-ctl --list-formats-ext

Результат команды (пример):

ioctl: VIDIOC_ENUM_FMT  
       Type: Video Capture  

       [0]: 'MJPG' (Motion-JPEG, compressed)  
               Size: Discrete 1280x720  
                       Interval: Discrete 0.033s (30.000 fps)  
               Size: Discrete 800x600  
                       Interval: Discrete 0.033s (30.000 fps)  
               Size: Discrete 640x480  
                       Interval: Discrete 0.033s (30.000 fps)  
               Size: Discrete 320x240  
                       Interval: Discrete 0.033s (30.000 fps)  
       [1]: 'YUYV' (YUYV 4:2:2)  
               Size: Discrete 1280x720  
                       Interval: Discrete 0.100s (10.000 fps)  
               Size: Discrete 800x600  
                       Interval: Discrete 0.050s (20.000 fps)  
               Size: Discrete 640x480  
                       Interval: Discrete 0.033s (30.000 fps)  
               Size: Discrete 320x240  
                       Interval: Discrete 0.033s (30.000 fps)

Разбор информации:

  • Формат MJPG (Motion-JPEG, сжатый) — обеспечивает более высокую частоту кадров (30 fps) для всех разрешений;

  • Формат YUYV — передаёт данные в «сыром» виде, без использования алгоритмов сжатия (видео занимает больше места) и имеет ограниченную частоту кадров для высоких разрешений;

  • Discrete 1280×720, Interval: Discrete 0.033s (30.000 fps) — пример разрешения экрана и интервала, который соответствует количеству кадров в секунду (указан в скобках) для данного разрешения.

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

Проверяю получение изображения с веб-камеры:

ffplay -f v4l2 -video_size 1280x720 -i /dev/video0
Проверка работы камеры
Проверка работы камеры

Разбор команды:

  • -f v4l2 — указывает формат захвата видео (в данном случае Video4Linux2);

  • -video_size 1280x720 — задаёт разрешение экрана (например, 1280×720);

  • -i /dev/video0 — указывает устройство, с которого будет захватываться видео.

Из информации в терминале видно, что для формата YUYV разрешение экрана соответствует 10 fps. Это подтверждается данными команды v4l2-ctl --list-formats-ext для указанного разрешения. Также появилось изображение с камеры, на котором видна моя гусеничная DIY-платформа. Изображение обновляется в реальном времени. Камера работает корректно, поэтому можно приступать к проверке видеострима.

Шаг 5: Запуск видеострима в MJPG-Streamer

Запускаю видеострим:

mjpg_streamer -i "input_uvc.so -d /dev/video0 -r 640x480 -f 15 -q 80" -o "output_http.so -p 8093 -w /usr/local/share/mjpg-streamer/www" &

Разбор команды:

  • nput_uvc.so — модуль для обработки UVC-видео;

  • -d /dev/video0 — указывает устройство камеры;

  • -r 640x480 и -f 15 — задают разрешение и частоту кадров;

  • -q 80 — качество JPEG;

  • output_http.so — модуль для трансляции через HTTP;

  • -p 8093 — порт для HTTP-сервера;

  • & — запускает процесс в фоне.

Видеострим запущен, осталось проверить его корректную работу.

Шаг 6: Проверка видеопотока

Открываю в браузере URL: http://localhost:8093/? action=stream:

Проверка видеопотока
Проверка видеопотока

На экране отображается изображение с камеры с частотой 15 fps, при которой видеопоток работает приемлемо. Для слабых устройств (например, Orange Pi Zero H2+) 15 fps достаточно. Более точные настройки для видео подберу на конкретном устройстве в ходе проверки. На мощных устройствах я могу установить до 30 fps для своей веб-камеры.

Стек технологий

Я подобрал следующий стек технологий для веб-приложения:

  • Python 3.12.0 — язык программирования, выбранный для разработки;

  • MJPG-Streamer — инструмент для стриминга видео через HTTP;

  • FastAPI — высокопроизводительный веб-фреймворк с поддержкой асинхронности и Websocket;

  • Uvicorn — лёгкий и быстрый сервер ASGI для Python, предназначенный для запуска современных веб-приложений и поддерживающий асинхронные операции;

  • Poetry — инструмент для управления зависимостями и сборки проектов на Python;

  • Pyenv — утилита для управления версиями Python, которая позволяет устанавливать, переключать и администрировать несколько версий Python на одной системе.

Перед началом написания кода я установил всё перечисленное:

  • Ссылка на установку Python;

  • Ссылка на документацию с процессом установки Poetry;

  • Ссылка на документацию с процессом установки Pyenv.

Виртуальное окружение и установка библиотек

Я создал виртуальное окружение следующими командами:

pyenv install 3.12.0
pyenv virtualenv 3.12.0 web_robot_control
pyenv versions
source ~/.bash_profile
pyenv shell web_robot_control

Разбор команд:

  • pyenv install 3.12.0 — установка версии Python 3.12.0 с использованием Pyenv;

  • pyenv virtualenv 3.12.0 web_robot_control — создание виртуального окружения с указанной версией Python и названием;

  • pyenv versions — отображение списка доступных виртуальных окружений и установленных версий Python;

  • source ~/.bash_profile — активация новых настроек для использования Pyenv;

  • pyenv shell web_robot_control — активация виртуального окружения в терминале.

Теперь у проекта есть виртуальная среда, в которую будут установлены все библиотеки и зависимости.

Устанавливаю Poetry в активированное виртуальное окружение web_robot_control:

pip install poetry

Устанавливаю фреймворк FastApi:

poetry add "fastapi[all]"

При установке fastapi[all] автоматически включаются uvicorn и другие важные библиотеки. Теперь, когда установлены все библиотеки и зависимости для них, я могу приступать к работе над самим веб-приложением.

Структура проекта

Структура проекта — это логическая организация файлов и папок внутри проекта, которая помогает упростить разработку, поддержку и расширение приложения. Poetry умеет создавать новый проект с готовой стартовой структурой, что очень удобно.

Создаю новый проект:

poetry new web-robot-control

Команда создала проект со следующей структурой:

web-robot-control
├── pyproject.toml
├── README.md
├── src
│   └── web_robot_control
│       └── init.py
└── tests
    └── init.py

Разбор структуры проекта:

  • pyproject.toml — файл, который описывает конфигурацию проекта. В нем можно указать зависимости, инструменты для сборки и настройки (используется в poetry, pip, setuptools);

  • README.md — текстовый файл с описанием проекта. В нем обычно содержатся инструкции по установке, использованию и дополнительной информации;

  • src/ — папка с исходным кодом приложения:

    • web_robot_control/ — основная директория кода проекта;

    • init.py — файл, который делает эту директорию пакетом Python. Он может быть пустым или содержать код для инициализации пакета.

  • tests/ — папка с тестами для проверки работоспособности кода.

Содержимое pyproject.toml:

[project]
name = "web-robot-control"
version = "0.1.0"
description = "Web-robot-control - open source веб-приложение для управлением роботом и трансляции видео с веб-камеры."
authors = [
    {name = "Arduinum628",email = "message.chaos628@gmail.com"}
]
license = {text = "MIT"}
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "fastapi[all] (>=0.115.12,<0.116.0)"
]

[tool.poetry]
packages = [{include = "web_robot_control", from = "src"}]

[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"

Разбор содержимого:

  • name = «web-robot-control» — название проекта;

  • version = »0.1.0» — текущая версия проекта;

  • description = «Web-robot-control — open source веб-приложение для управлением роботом и трансляции видео с веб-камеры.» — краткое описание проекта;

  • authors = [{name = «Arduinum628», email = «message.chaos628@gmail.com»}] — имя автора и его электронный адрес;

  • license = {text = «MIT»} — тип лицензии, в данном случае MIT (открытое ПО);

  • readme = «README.md» — путь к README-файлу проекта;

  • requires-python = »>=3.12» — требуемая версия Python (не ниже 3.12);

  • dependencies = [«fastapi[all] (>=0.115.12, <0.116.0)" — список зависимостей, включая FastAPI;

  • packages = [{include = «name_project_packet», from = «src»}] — указание пакетов проекта и их пути;

  • requires = [«poetry-core>=2.0.0, <3.0.0"] — зависимости, необходимые для сборки проекта;

  • build-backend = «poetry.core.masonry.api» — механизм сборки для создания пакета.

Теперь у проекта есть настроенный конфигурационный файл. Подробнее о файле pyproject.toml можно узнать из документации Poetry.

Написание кода и документации веб-приложения

Теперь, когда настроено виртуальное окружение, установлены все зависимости и создана структура проекта, я перехожу к написанию кода и документации.

Документация

Перед началом разработки важно установить правила оформления веток и коммитов. Поскольку это open-source проект, другие разработчики, которые захотят дополнить приложение, должны иметь чёткие рекомендации по работе с ветками и коммитами, а также описание проекта. Это помогает организовать процесс разработки, избежать хаоса и понимать разработчикам и пользователям для чего приложение написано. Документация хранится в файле README.md, который уже был ранее создан командой poetry new web-robot-control. Осталось его наполнить содержимым.

Содержимое README.md:

# Web-robot-control

**Web-robot-control** - open source веб-приложение для управлением роботом и трансляции видео с веб-камеры.

## Запуск приложения

**Запуск для локальной разработки (бекенд)**: `poetry run uvicorn web_robot_control.main:app --host server_ip --port port_number`


**Todo:** создать Python-функцию для запуска веб-приложения и добавить её в скрипты Poetry

Как оформлять ветки и коммиты Пример ветки `user_name/name_task` - **user_name** (имя пользователя); - **name_task** (название задачи). Пример коммита `refactor: renaming a variable` - **feat:** (новая функционал кода, БЕЗ учёта функционала для сборок); - **devops:** (функционал для сборки, - добавление, удаление и исправление); - **fix:** (исправление ошибок функционального кода); - **docs:** (изменения в документации); - **style:** (форматирование, отсутствующие точки с запятой и т.п., без изменения производственного кода); - **refactor:** (рефакторинг производственного кода, например, переименование переменной); - **test:** (добавление недостающих тестов, рефакторинг тестов; без изменения производственного кода); - **chore:** (обновление рутинных задач и т. д.; без изменения производственного кода). Оформление основано на https://www.conventionalcommits.org/en/v1.0.0/

Правила оформления веток и коммитов основаны на спецификации Conventional Commits. Это помогает структурировать изменения, чтобы они были понятными, единообразными и легко воспринимаемыми.

Кроме того, я добавил описание проекта, чтобы ясно объяснить, что это за приложение и для каких целей оно создано. Также включён пример команды для локального запуска приложения:

poetry run uvicorn web_robot_control.main:app --host server_ip --port port_number

.env.example

Я создал файл .env.example, который служит для описания переменных окружения.

STREAM_URL="url-адрес видеопотока"

В реальном .env файле я добавлю url-адрес видеопотока:

STREAM_URL=http://localhost:8093/?action=stream

settings.py

Я создал файл settings.py для хранения настроек проекта, прочитанных из файла .env, и валидации типов переменных окружения. Для этих целей используется библиотека pydantic_settings. Она предоставляет готовые классы для валидации, чтения .env файла и других задач.

Содержимое settings.py:

from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
    """Класс для данных конфига"""
model_config = SettingsConfigDict(
        env_file = '.env',
        env_file_encoding='utf-8',
        extra='ignore'
    )
 stream_url: str
settings = Settings()

Пояснения к коду settings.py:

  • class Settings(BaseSettings) — класс, который сохраняет переменные окружения, загруженные из .env файла, для настройки проекта;

  • model_config = SettingsConfigDict(...) — переменная для задания конфигурации модели:

  • env_file='.env' — указывает, что настройки загружаются из файла .env;

  • env_file_encoding='utf-8' — задаёт кодировку файла .env;

  • extra='ignore' — игнорирует переменные, которые не описаны в классе;

  • stream_url: str — переменная для хранения URL-видеострима;

  • settings = Settings() — создание экземпляра класса с настройками.

views.py

Теперь я приступаю к созданию самого веб-приложения, начиная с файла views.py. Этот файл обрабатывает запросы и отправляет ответы клиенту.

FastAPI поддерживает WebSocket, который я использую для получения команд с фронтенда в реальном времени. Это решение не мешает видеостримингу.  Более подробно ознакомиться с WebSocket можно узнать на странице документации.

Содержимое views.py:

from fastapi import APIRouter, WebSocket
from fastapi.requests import Request
from fastapi.responses import HTMLResponse, Response
from fastapi.templating import Jinja2Templates

from starlette.websockets import WebSocketDisconnect
import httpx

from web_robot_control.settings import settings


# Создаем объект роутера
router = APIRouter()

# Создаём объект для рендеринга html-шаблонов
templates = Jinja2Templates(directory='static')

@router.get('/', response_class=HTMLResponse)
async def index(request: Request) -> Response:
    """
    Асинхронная функция для получения главной страницы приложения.
    """

    return templates.TemplateResponse(
        request=request,
        name='index.html',
        context={'title': 'Web-robot-control - Главная', 'name_robot': 'Bot1'}
    )


@router.get('/config')
async def get_config() -> dict:
    """Aсинхронная функция для получения stream_url."""

    return {'stream_url': settings.stream_url}


@router.websocket('/ws')
async def websocket_endpoint(websocket: WebSocket) -> None:
    # Установка содединения по веб-сокету
    await websocket.accept()

    try:
        async with httpx.AsyncClient() as client:
            while True:
                # Получение команды от клиента (с веб-сокета)
                command = await websocket.receive_text()
                print(f'Получена команда: {command}')

                # Todo: здесь будет логика валидации команд

                # Todo: здесь будет логика обработки команды

    except WebSocketDisconnect:
        print('WebSocket отключен')  # Todo: для вывода ошибок будет настроен logger
    # Todo: вместо Exception будут добавлена ловля других ошибок
    # (после того как функция будет полностью дописана)
    except Exception as err:
        err_text = f'Ошибка: {str(err)}'
        await websocket.send_text(err_text)
        print(err_text)

Импорты и инициализация объектов:

  • from fastapi import APIRouter, WebSocket — модули для работы с маршрутами HTTP и WebSocket;

  • from fastapi.responses import HTMLResponse — класс для возврата HTML-контента в HTTP-ответе;

  • from starlette.websockets import WebSocketDisconnect — исключение для обработки отключения WebSocket соединения;

  • import httpx — библиотека для выполнения HTTP-запросов асинхронно;

  • from web_robot_control.settings import settings — настройки приложения (например, переменные из .env);

  • router = APIRouter() — объект роутера;

  • templates = Jinja2Templates(directory='static') — объект для рендеринга HTML-шаблонов.

index () — функция для отображения главной страницы:

  • @router.get('/', response_class=HTMLResponse) — связывает функцию с URL '/' для GET-запроса и задаёт тип ответа HTMLResponse;

  • return templates.TemplateResponse(...) — возвращает HTML-шаблон с контекстом:

    • request=request — передаёт запрос в шаблонизатор;

    • name='index.html' — указывает имя HTML-шаблона;

    • context={'title': …, 'name_robot': …} — передаёт данные для использования в шаблоне.

get_config () — функция для получения настроек:

  • @router.get('/config') — связывает функцию с URL /config для GET-запроса;

  • return {'stream_url': settings.stream_url} — возвращает словарь с URL видеострима в виде JSON-ответа.

websocket_endpoint () — функция для обработки команд через WebSocket:

  • @router.websocket('/ws') — связывает функцию с маршрутом /ws для обработки WebSocket-запросов;

  • await websocket.accept() — устанавливает соединение между сервером и клиентом;

  • async with httpx.AsyncClient() as client: — создаёт асинхронного HTTP-клиента (пока заготовка);

  • command = await websocket.receive_text() — получает команду от клиента через WebSocket;

  • print(f'Получена команда: {command}') — вывод команды в консоль (для тестирования);

  • except WebSocketDisconnect: — ловит разрыв соединения и выводит сообщение;

  • except Exception as err: — ловит общие ошибки и отправляет их клиенту через WebSocket.

Теперь у бекенда есть возможность получать команды от клиента (фронтенда), передавать данные конфига на фронтенд через WebSocket и получать html главной страницы.

main.py

Файл main.py — это главный файл приложения, который отвечает за его создание и запуск. В этом файле подключаются маршруты, статические файлы и другие компоненты приложения.

Содержимое main.py:

from fastapi import FastAPI
from starlette.staticfiles import StaticFiles

from web_robot_control.views import router


# создаем экземпляр FastAPI
app = FastAPI()

# подключаем статические файлы
app.mount('/static', StaticFiles(directory='static'), name='static')

# подключаем роутер
app.include_router(router)

Импорты:

  • from fastapi import FastAPI — импорт класса FastAPI, который используется для создания основного приложения;

  • from starlette.staticfiles import StaticFiles — импорт класса StaticFiles, который отвечает за обслуживание статических файлов (например, CSS, JavaScript, изображения);

  • from web_robot_control.views import router — импорт маршрутизатора router, в котором определены обработчики запросов (созданные ранее в views.py).

Приложение в FastApi:

  • app = FastAPI() — создание экземпляра основного приложения;

  • app.mount('/static', StaticFiles(directory='static'), name='static'):

    • '/static' — URL, по которому будут доступны статические файлы;

    • StaticFiles (directory='static') — указывает путь к папке с статическими файлами;

    • name='static' — имя маршрута, позволяющее ссылаться на него в других частях приложения;

  • app.include_router(router) — подключение маршрутизатора, который добавляет маршруты из views.py в приложение.

Бекендная часть готова, теперь осталось написать фронтенд для веб-приложения.

index.html

Я создаю простой фронтенд, который будет выполнять роль клиента. Сначала я разработал файл index.html, предназначенный для отображения кнопок управления и видеострима. Я буду использовать Bootstrap для ускорения и упрощения разработки фронтенда. Bootstrap — это популярный фреймворк для создания адаптивных веб-интерфейсов, который включает готовые стили, компоненты и JavaScript-инструменты для ускорения разработки.

Содержимое index.html:




    
    
    {{ title }}
    
    
    
    


    
       

Управление роботом {{ name_robot }}

               
           
               
                   
                        Видеопоток                    
                   
                                       
                       
                                                                                   
                                                                                                                                                                                                                           
                       
                   
               
           
       
   
   

Главные особенности index.html:

  • {{ title }} — шаблонное выражение для динамического указания заголовка страницы (его передали в context на бэкенде);

  • href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> — подключение CSS-файла из CDN Bootstrap для применения стилей для приложения;

  • — подключение локального CSS-файла (style.css), где находятся пользовательские стили;

  •  — подключение локального JavaScript-файла (command.js), который управляет командами (например, отправка команд WebSocket’у).

  • Управление роботом {{ name_robot }}

     — Шаблонное выражение для вывода имени робота и красивое оформление заголовка (выравнивание по центру).

  • Видеопоток — Элемент для отображения видеопотока от робота, где src будет динамически обновляться через JavaScript;

  •  — cекция с кнопками управления роботом (вперёд, назад, влево, вправо).

style.css

Я добавил пользовательские стили, чтобы слегка кастомизировать стандартные стили Bootstrap.

Содержимое style.css:

/* Задает общий фон страницы /
body {
    background-color: #f8f9fa;
}

/ Создает тень для карточки, добавляя глубину и визуальную привлекательность /
.card {
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
/ Устанавливает черный цвет фона для элементов с классами card-command и card-body /
.card-command, .card-body {
    background-color: black;
}
/ Устанавливает стили для блока card-command /
.card-command {
    width: 100%; / Задает ширину в 100% /
    border-top-left-radius: 0; / Убирает закругление верхнего левого угла /
    border-top-right-radius: 0; / Убирает закругление верхнего правого угла /
}

/ Определяет стили для горизонтальной линии между элементами /
.line {
    background-color: #ffc107; / Задает желтый цвет линии /
    height: 6px; / Высота линии /
    width: 100%; / Линия занимает всю ширину блока /
}

/ Определяет максимальную ширину изображения и делает его адаптивным /
img {
    max-width: 100%; / Ограничивает ширину изображения, чтобы оно не выходило за пределы контейнера /
    height: auto; / Автоматически изменяет высоту изображения пропорционально ширине */
}

command.js

Теперь, когда файлы index.html и style.css для главной страницы готовы, я добавил логику для работы клиента. Для этого я создал файл command.js, который отвечает за обновление видеострима, вывод сообщений в консоль для отладки, а также управление WebSocket и передачу команд бекенду.

Содержимое command.js:

// Ждём, пока загрузится весь контент DOM
document.addEventListener("DOMContentLoaded", () => {
    // Создаём WebSocket-соединение с сервером
    const ws = new WebSocket(ws://${window.location.host}/ws);

    // Делаем запрос к серверу на эндпоинт "/config"
    fetch('/config')
        .then(response => response.json()) // Преобразуем ответ в JSON
        .then(data => {
            // Получаем значение stream_url из ответа
            const streamUrl = data.stream_url;
            const videoElement = document.getElementById("video-stream");
            // Устанавливаем URL видеопотока в элемент 

Особенность моего управления в том, что команда отправляется каждые 10 мс, пока пользователь удерживает кнопку. Задержку уточним экспериментально на реальном роботе. Отправка команд прекращается, как только кнопка отпускается.

Ссылка на получившийся в итоге open-source проект web-robot-control.

Итоговая cтруктура проекта

Структура теперь имеет следующий вид:

web-robot-control
├── src
│   └── web_robot_control
│       ├── pycache
│       ├── init.py
│       ├── main.py
│       ├── settings.py
│       ├── views.py
│   └── static
│       ├── command.js
│       ├── index.html
│       ├── style.css
└── tests

    └── init.py

├── .env
├── .env.example
├── .gitignore
├── LICENSE
├── poetry.lock
├── pyproject.toml
└── README.md

Структура проекта соответствует принципу разделению ответственности файлов. Одна из его функций — определять, где должен находится каждый файл, чтобы код оставался упорядоченным, удобным для поддержки и расширения.

Web-приложение в действии

Запускаю веб-приложение:

poetry run uvicorn web_robot_control.main:app --host localhost --port 8095

Разбор команды:

  • poetry run — запускает команду внутри виртуальной среды, созданной Poetry;

  • uvicorn web_robot_control.main:app — запускает ASGI-сервер Uvicorn, указывая:

    • web_robot_control.main — модуль Python, где находится приложение;

    • app — экземпляр приложения FastAPI, который будет запущен.

  • --host localhost — сервер будет слушать локальный хост, то есть доступен только на текущем устройстве;

  • --port 8095 — приложение будет запущено на порту 8095.

Работа веб-приложения в браузере по URL http://localhost:8095:

На видео видно, что видеострим успешно работает в реальном времени, а команды «left», «right», «forward», «backward» отображаются в консоли браузера. Я управляю роботом обычным ИК-пультом.

Работа бэкенда:

INFO:     Started server process [43146]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://localhost:8095 (Press CTRL+C to quit)
INFO:     ::1:37202 - "GET / HTTP/1.1" 200 OK
INFO:     ::1:37202 - "GET /static/command.js HTTP/1.1" 304 Not Modified
INFO:     ('::1', 47800) - "WebSocket /ws" [accepted]
INFO:     ::1:37202 - "GET /config HTTP/1.1" 200 OK
INFO:     connection open
Получена команда: forward
Получена команда: right
Получена команда: backward
Получена команда: left
^CINFO:     Shutting down
WebSocket отключен
INFO:     connection closed
INFO:     Waiting for application shutdown.
INFO:     Application shutdown complete.
INFO:     Finished server process [17634]

Бэкенд успешно принимает команды и выводит результат в терминал:

  • Получена команда: forward;

  • Получена команда: right;

  • Получена команда: backward;

  • Получена команда: left.

Заключение и планы на будущее

Были установлены все необходимые зависимости и библиотеки для видеострима. Видеострим с веб-камеры успешно протестирован. Настроено виртуальное окружение, а также установлены зависимости для проекта. Написан код, обеспечивающий отправку команд на сервер и отображение видеострима. В результате получилась достойная основа для open-source проекта, которая пока ещё не достигла статуса MVP1 и требует доработки.

Хочу добавить в проект logger — инструмент для ведения логов, который будет помогать отслеживать события во время работы программы. Также планирую реализовать валидацию команд и механизм их отправки роботу. Кроме того, нужно добавить утилиту для робота, которая будет принимать команды с сервера. Без неё робот не получит команды управления. Если у вас есть идеи по развитию проекта, поделитесь ими в комментариях — буду рад услышать ваши предложения!

Автор статьи @Arduinum

НЛО прилетело и оставило здесь промокод для читателей нашего блога:
— 15% на заказ любого VDS (кроме тарифа Прогрев) — HABRFIRSTVDS.

© Habrahabr.ru