feat: added static for main page, added views.py: index, get_condig, websocket_endpoint (functionality for receiving commands from the frontend), added main.py: creating an application, connecting static and connecting a router, added Settings.py: class Settings with model_config and stream_url

This commit is contained in:
Arduinum628
2025-05-06 13:39:18 +03:00
parent ffd101e454
commit 1d20e1c6ac
11 changed files with 1298 additions and 908 deletions

1
.env.example Normal file
View File

@@ -0,0 +1 @@
STREAM_URL="url адрес видеопотока"

4
.gitignore vendored
View File

@@ -172,3 +172,7 @@ cython_debug/
# PyPI configuration file
.pypirc
# IDE
.idea/
.vscode/

View File

@@ -2,6 +2,12 @@
**Web-robot-control** - open source веб-приложение для управлением роботом и трансляции видео с веб-камеры.
# Запуск приложения
**Запуск для локальной разработки**: `poetry run uvicorn web_robot_control.main:app --host server_ip
--port port_number`
# Todo: создать Python-функцию для запуска веб-приложения и добавить её в скрипты Poetry
<details>
<summary>
<strong>

1922
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@ license = {text = "MIT"}
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"streamlit (>=1.44.1,<2.0.0)"
"fastapi[all] (>=0.115.12,<0.116.0)"
]
[tool.poetry]

View File

@@ -0,0 +1,14 @@
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)

View File

@@ -0,0 +1,16 @@
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()

View File

@@ -0,0 +1,61 @@
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)

89
static/command.js Normal file
View File

@@ -0,0 +1,89 @@
// Ждём, пока загрузится весь контент 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 видеопотока в элемент <video>
videoElement.src = streamUrl;
});
// Обработчик события открытия WebSocket-соединения
ws.onopen = function() {
console.log("WebSocket подключен");
};
// Обработчик события получения сообщения по WebSocket
ws.onmessage = function(event) {
console.log("Получено:", event.data); // Выводим полученные данные
};
// Обработчик события закрытия WebSocket-соединения
ws.onclose = function() {
console.log("WebSocket закрыт");
};
// Обработчик ошибок WebSocket
ws.onerror = function(error) {
console.log("WebSocket ошибка:", error); // Логируем ошибку
};
let commandInterval;
// Функция для начала отправки команды с заданным интервалом
function startSendingCommand(command) {
// Отправляем команду сразу
sendCommand(command);
// Запускаем интервал для повторной отправки команды
commandInterval = setInterval(() => {
sendCommand(command); // Повторяем отправку команды
}, 10); // Интервал отправки — каждые 10 мс
}
// Функция для остановки отправки команд
function stopSendingCommand() {
// Останавливаем интервал
clearInterval(commandInterval);
}
// Функция для отправки команды через WebSocket
function sendCommand(command) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(command); // Отправляем команду через WebSocket
console.log("Команда:", command); // Логируем отправленную команду
} else {
console.log("WebSocket не подключён"); // Если WebSocket не открыт
}
}
// Назначение обработчиков событий для кнопки "Вперёд"
const forwardButton = document.getElementById("forward-button");
forwardButton.addEventListener("mousedown", () => startSendingCommand("forward")); // Начало отправки команды
forwardButton.addEventListener("mouseup", stopSendingCommand); // Остановка отправки при отпускании кнопки
forwardButton.addEventListener("mouseleave", stopSendingCommand); // Остановка отправки, если курсор уходит с кнопки
// Назначение обработчиков событий для кнопки "Влево"
const leftButton = document.getElementById("left-button");
leftButton.addEventListener("mousedown", () => startSendingCommand("left"));
leftButton.addEventListener("mouseup", stopSendingCommand);
leftButton.addEventListener("mouseleave", stopSendingCommand);
// Назначение обработчиков событий для кнопки "Вправо"
const rightButton = document.getElementById("right-button");
rightButton.addEventListener("mousedown", () => startSendingCommand("right"));
rightButton.addEventListener("mouseup", stopSendingCommand);
rightButton.addEventListener("mouseleave", stopSendingCommand);
// Назначение обработчиков событий для кнопки "Назад"
const backwardButton = document.getElementById("backward-button");
backwardButton.addEventListener("mousedown", () => startSendingCommand("backward"));
backwardButton.addEventListener("mouseup", stopSendingCommand);
backwardButton.addEventListener("mouseleave", stopSendingCommand);
});

57
static/index.html Normal file
View File

@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
<link rel="stylesheet" href="/static/style.css">
<script src="/static/command.js"></script>
</head>
<body>
<div class="container mt-5">
<h1 class="text-center mb-4">Управление роботом {{ name_robot }}</h1>
<!-- Видеопоток -->
<div class="row justify-content-center">
<div class="col-md-7 px-0">
<div class="card">
<div class="card-body text-center">
<img src="" class="img-fluid" id="video-stream" alt="Видеопоток">
</div>
<div class="line"></div>
<!-- Кнопки -->
<div class="col-md-12 d-flex align-items-center px-0">
<div class="card-command card d-flex flex-column justify-content-center align-items-center">
<!-- Вверх -->
<button class="btn btn-warning m-1" id="forward-button">
<i class="bi bi-arrow-up"></i>
</button>
<div class="d-flex">
<!-- Влево -->
<button class="btn btn-warning m-1" id="left-button">
<i class="bi bi-arrow-left"></i>
</button>
<!-- Вниз -->
<button class="btn btn-warning m-1" id="backward-button">
<i class="bi bi-arrow-down"></i>
</button>
<!-- Вправо -->
<button class="btn btn-warning m-1" id="right-button">
<i class="bi bi-arrow-right"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

34
static/style.css Normal file
View File

@@ -0,0 +1,34 @@
/* Задает общий фон страницы */
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; /* Автоматически изменяет высоту изображения пропорционально ширине */
}