From 72a50bab9a8167b70e8ec99168f42da21eb2406c Mon Sep 17 00:00:00 2001 From: blinker Date: Sat, 4 Jul 2026 12:27:22 +0000 Subject: [PATCH 1/6] Add homework_12 folder from course repo --- homework_12/.gitignore | 5 + homework_12/edge/c2/.gitignore | 1 + homework_12/edge/c2/CMakeLists.txt | 32 +++ homework_12/edge/c2/Dockerfile | 52 +++++ homework_12/edge/c2/src/c2_controller.cpp | 43 ++++ homework_12/edge/c2/src/c2_controller.hpp | 32 +++ homework_12/edge/c2/src/fc_link.cpp | 104 +++++++++ homework_12/edge/c2/src/fc_link.hpp | 77 ++++++ homework_12/edge/c2/src/main.cpp | 36 +++ homework_12/edge/c2/src/udp_socket.hpp | 70 ++++++ homework_12/edge/config/c2_config.json | 3 + homework_12/edge/docker-compose.yml | 31 +++ homework_12/qgc/README.md | 54 +++++ homework_12/qgc/lyman-patrol.plan | 271 ++++++++++++++++++++++ homework_12/sim/Dockerfile | 23 ++ homework_12/sim/compose.sitl.yml | 24 ++ homework_12/sim/entrypoint.sh | 12 + 17 files changed, 870 insertions(+) create mode 100644 homework_12/.gitignore create mode 100644 homework_12/edge/c2/.gitignore create mode 100644 homework_12/edge/c2/CMakeLists.txt create mode 100644 homework_12/edge/c2/Dockerfile create mode 100644 homework_12/edge/c2/src/c2_controller.cpp create mode 100644 homework_12/edge/c2/src/c2_controller.hpp create mode 100644 homework_12/edge/c2/src/fc_link.cpp create mode 100644 homework_12/edge/c2/src/fc_link.hpp create mode 100644 homework_12/edge/c2/src/main.cpp create mode 100644 homework_12/edge/c2/src/udp_socket.hpp create mode 100644 homework_12/edge/config/c2_config.json create mode 100644 homework_12/edge/docker-compose.yml create mode 100644 homework_12/qgc/README.md create mode 100644 homework_12/qgc/lyman-patrol.plan create mode 100644 homework_12/sim/Dockerfile create mode 100644 homework_12/sim/compose.sitl.yml create mode 100644 homework_12/sim/entrypoint.sh diff --git a/homework_12/.gitignore b/homework_12/.gitignore new file mode 100644 index 0000000..2386bdc --- /dev/null +++ b/homework_12/.gitignore @@ -0,0 +1,5 @@ +# Логи C2 мають лишатися на машинi, де запускався compose stack. +edge/logs/ + +# Викладацькi demo-файли та solution code не є частиною student-facing repo. +_teacher/ diff --git a/homework_12/edge/c2/.gitignore b/homework_12/edge/c2/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/homework_12/edge/c2/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/homework_12/edge/c2/CMakeLists.txt b/homework_12/edge/c2/CMakeLists.txt new file mode 100644 index 0000000..8591e35 --- /dev/null +++ b/homework_12/edge/c2/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.20) +project(c2_service CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +find_package(MAVSDK REQUIRED) + +include(FetchContent) + +FetchContent_Declare(nlohmann_json + URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz +) +set(JSON_BuildTests OFF CACHE BOOL "" FORCE) +set(JSON_Install OFF CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(nlohmann_json) + +add_executable(c2_service + src/main.cpp + src/fc_link.cpp + src/c2_controller.cpp +) + +target_include_directories(c2_service PRIVATE ${CMAKE_SOURCE_DIR}/src) + +target_link_libraries(c2_service PRIVATE + MAVSDK::mavsdk + nlohmann_json::nlohmann_json +) + +target_compile_options(c2_service PRIVATE -Wall -Wextra) diff --git a/homework_12/edge/c2/Dockerfile b/homework_12/edge/c2/Dockerfile new file mode 100644 index 0000000..e4a2c28 --- /dev/null +++ b/homework_12/edge/c2/Dockerfile @@ -0,0 +1,52 @@ +# Етап 1: збирання +# MAVSDK у цьому ДЗ зафiксовано на конкретнiй версiї, щоб не шукати пакет вручну. +ARG MAVSDK_VERSION=2.12.9 + +FROM _____ AS builder + +ARG MAVSDK_VERSION +ENV DEBIAN_FRONTEND=noninteractive + +# Блок залежностей надано: iнструменти збирання, CMake та wget для MAVSDK. +RUN apt-get update && apt-get install -y --no-install-recommends \ + g++ cmake git make wget ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# MAVSDK встановлюється з готового .deb пакета для Ubuntu 22.04. +RUN wget -qO /tmp/mavsdk-dev.deb \ + "https://github.com/mavlink/MAVSDK/releases/download/v${MAVSDK_VERSION}/libmavsdk-dev_${MAVSDK_VERSION}_ubuntu22.04_amd64.deb" \ + && dpkg -i /tmp/mavsdk-dev.deb \ + && rm /tmp/mavsdk-dev.deb + +WORKDIR /src +COPY . . + +# Заповнити команду CMake-збирання. +RUN _____ + +# Етап 2: запуск +# Для простоти ДЗ той самий MAVSDK .deb встановлюється i в образ запуску. +FROM _____ + +ARG MAVSDK_VERSION +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + wget ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +RUN wget -qO /tmp/mavsdk.deb \ + "https://github.com/mavlink/MAVSDK/releases/download/v${MAVSDK_VERSION}/libmavsdk-dev_${MAVSDK_VERSION}_ubuntu22.04_amd64.deb" \ + && dpkg -i /tmp/mavsdk.deb \ + && rm /tmp/mavsdk.deb \ + && apt-get clean + +RUN mkdir -p /etc/c2 /var/log/c2 + +WORKDIR /app +COPY --from=builder /src/build/c2_service . + +HEALTHCHECK --interval=5s --timeout=2s --retries=3 \ + CMD test -f /tmp/c2_healthy || exit 1 + +CMD ["./c2_service"] diff --git a/homework_12/edge/c2/src/c2_controller.cpp b/homework_12/edge/c2/src/c2_controller.cpp new file mode 100644 index 0000000..61ee898 --- /dev/null +++ b/homework_12/edge/c2/src/c2_controller.cpp @@ -0,0 +1,43 @@ +#include "c2_controller.hpp" +#include "fc_link.hpp" // MAVSDK обгортка, API описано у fc_link.hpp +#include "udp_socket.hpp" // UDP прийом, API описано у udp_socket.hpp + +#include // Розбiр JSON з точками маршруту вiд auto_stub + +#include +#include +#include + +static constexpr uint16_t STUB_PORT = 14560; + +struct C2Controller::Impl { + C2State state = C2State::DISARMED; + + // TODO: додати FcLink, UdpSocket, лог-файл та прапорцi стану. + // FcLink потребує fc_port у конструкторi Impl. + // UdpSocket має слухати STUB_PORT. + + void transition(C2State next) { + // TODO: якщо next != state, записати "PREV -> NEW" у stdout i лог, + // потiм оновити state. Якщо стан не змiнився, нiчого не писати. + (void)next; + } +}; + +C2Controller::C2Controller(uint16_t fc_port) + : impl_(std::make_unique()) +{ + // TODO: передати fc_port в Impl та вiдкрити /var/log/c2/c2.log. + (void)fc_port; +} + +C2Controller::~C2Controller() = default; + +void C2Controller::tick() { + // TODO: healthcheck, оновлення C2State, читання точки маршруту, + // передавання або блокування команди згiдно з поточним станом. +} + +C2State C2Controller::current_state() const { + return impl_->state; +} diff --git a/homework_12/edge/c2/src/c2_controller.hpp b/homework_12/edge/c2/src/c2_controller.hpp new file mode 100644 index 0000000..f3f3c4f --- /dev/null +++ b/homework_12/edge/c2/src/c2_controller.hpp @@ -0,0 +1,32 @@ +#pragma once +#include +#include +#include + +enum class C2State { + DISARMED, + ARMED_HOLD, + ARMED_GUIDED, + ARMED_MANUAL, +}; + +// C2-сервiс для арбiтражу команд. +// Читає MAVLink-телеметрiю з fc_sim через MAVSDK та точки маршруту +// вiд auto_stub у JSON через UDP. Вирiшує, хто може командувати FC. +class C2Controller { +public: + explicit C2Controller(uint16_t fc_port); + ~C2Controller(); + + // Обробити одну iтерацiю: передати або блокувати команди + // залежно вiд поточного стану. Викликати з main loop приблизно на 10 Hz. + void tick(); + + // Read-only доступ до поточного стану C2. + // Корисно для перевiрок, тестiв або майбутньої дiагностики. + C2State current_state() const; + +private: + struct Impl; + std::unique_ptr impl_; +}; diff --git a/homework_12/edge/c2/src/fc_link.cpp b/homework_12/edge/c2/src/fc_link.cpp new file mode 100644 index 0000000..445368e --- /dev/null +++ b/homework_12/edge/c2/src/fc_link.cpp @@ -0,0 +1,104 @@ +// fc_link.cpp: готовий допомiжний файл, змiнювати не потрiбно. +#include "fc_link.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +struct FcLink::Impl { + mavsdk::Mavsdk mavsdk{mavsdk::Mavsdk::Configuration{mavsdk::Mavsdk::ComponentType::GroundStation}}; + std::shared_ptr system; + std::unique_ptr telemetry; + std::unique_ptr action; + std::unique_ptr offboard; + + std::atomic connected{false}; + std::atomic armed{false}; + std::atomic mode{FcLink::FlightMode::Unknown}; + + std::mutex offboard_mutex; + bool offboard_started = false; +}; + +FcLink::FcLink(uint16_t listen_port, std::chrono::seconds timeout) + : impl_(std::make_unique()) +{ + std::string conn = "udp://:" + std::to_string(listen_port); + const auto connection_result = impl_->mavsdk.add_any_connection(conn); + if (connection_result != mavsdk::ConnectionResult::Success) { + throw std::runtime_error("FcLink: cannot open " + conn + ", result=" + + std::to_string(static_cast(connection_result))); + } + + std::promise> prom; + auto fut = prom.get_future(); + + impl_->mavsdk.subscribe_on_new_system([this, &prom]() { + for (auto& s : impl_->mavsdk.systems()) { + if (s->has_autopilot()) { + try { prom.set_value(s); } catch (...) {} + return; + } + } + }); + + if (fut.wait_for(timeout) != std::future_status::ready) + throw std::runtime_error("FcLink: timeout: is ArduPilot sending MAVLink to this UDP port?"); + + impl_->system = fut.get(); + impl_->telemetry = std::make_unique(impl_->system); + impl_->action = std::make_unique(impl_->system); + impl_->offboard = std::make_unique(impl_->system); + + impl_->telemetry->subscribe_armed([this](bool a) { + impl_->connected = true; + impl_->armed = a; + if (!a) impl_->mode = FlightMode::Unknown; + }); + + impl_->telemetry->subscribe_flight_mode([this](mavsdk::Telemetry::FlightMode m) { + using FM = mavsdk::Telemetry::FlightMode; + FlightMode mapped = FlightMode::Unknown; + switch (m) { + case FM::Manual: mapped = FlightMode::Manual; break; + case FM::Hold: mapped = FlightMode::Hold; break; + case FM::Offboard: mapped = FlightMode::Guided; break; + default: break; + } + impl_->mode = mapped; + }); +} + +FcLink::~FcLink() = default; + +bool FcLink::is_connected() const { return impl_->connected; } +bool FcLink::is_armed() const { return impl_->armed; } +FcLink::FlightMode FcLink::flight_mode() const { return impl_->mode; } + +void FcLink::hold() { + impl_->action->hold(); +} + +void FcLink::go_to_ned(float north_m, float east_m, float down_m) { + { + std::lock_guard lock(impl_->offboard_mutex); + if (!impl_->offboard_started) { + impl_->offboard->set_position_ned({}); + impl_->offboard->start(); + impl_->offboard_started = true; + } + } + mavsdk::Offboard::PositionNedYaw pos{}; + pos.north_m = north_m; + pos.east_m = east_m; + pos.down_m = down_m; + pos.yaw_deg = 0.0f; + impl_->offboard->set_position_ned(pos); +} diff --git a/homework_12/edge/c2/src/fc_link.hpp b/homework_12/edge/c2/src/fc_link.hpp new file mode 100644 index 0000000..3aa5e31 --- /dev/null +++ b/homework_12/edge/c2/src/fc_link.hpp @@ -0,0 +1,77 @@ +#pragma once +// fc_link.hpp: готовий допомiжний файл, змiнювати не потрiбно. +// +// Тонка C++ обгортка над MAVSDK для комунiкацiї з ArduPilot. +// Ховає налаштування з'єднання, плагiни MAVSDK та потокобезпечнiсть. +// Пiдключити цей заголовок i використати FcLink у c2_controller.cpp. +// +// Потрiбний API: +// - is_connected(): чи отримано перший HEARTBEAT +// - is_armed(): чи FC зараз armed +// - flight_mode(): поточний режим руху +// - hold(): перейти у HOLD +// - go_to_ned(north_m, east_m): передати точку маршруту + +#include +#include +#include + +// FcLink пiдключається до ArduPilot FC через MAVSDK +// i надає простий API опитування для стану armed, режиму руху та MAVLink-команд. +// +// Типове використання: +// +// FcLink fc(14551); // блокується до вiдповiдi автопiлота (30 s) +// +// while (true) { +// if (fc.is_armed() && fc.flight_mode() == FcLink::FlightMode::Guided) +// fc.go_to_ned(50.0f, 0.0f); // передати точку маршруту +// else if (fc.is_armed()) +// fc.hold(); // перейти у HOLD mode +// std::this_thread::sleep_for(100ms); +// } +class FcLink { +public: + // Режим руху з ArduPilot HEARTBEAT. + // У ArduPilot це режим GUIDED. MAVSDK може показувати його як Offboard, + // тому FcLink нормалiзує це до FlightMode::Guided. + enum class FlightMode { Unknown, Manual, Hold, Guided }; + + // Пiдключитися до ArduPilot. + // ArduPilot має надсилати MAVLink UDP-пакети на 127.0.0.1:. + // Блокується до вiдповiдi автопiлота або до завершення часу очiкування. + // Кидає std::runtime_error, якщо з'єднання не вiдкрилось або минув час очiкування. + explicit FcLink(uint16_t listen_port, + std::chrono::seconds timeout = std::chrono::seconds{30}); + + ~FcLink(); + + FcLink(const FcLink&) = delete; + FcLink& operator=(const FcLink&) = delete; + + // Повертає true пiсля першого отриманого HEARTBEAT. + // Використати це як сигнал для перевiрки стану (/tmp/c2_healthy). + bool is_connected() const; + + // Повертає true, якщо FC зараз у станi armed. + bool is_armed() const; + + // Повертає поточний режим руху. Оновлюється приблизно раз на 1 s через HEARTBEAT. + FlightMode flight_mode() const; + + // Надiслати MAVLink-команду для переходу FC у HOLD mode. + // Безпечно викликати з будь-якого стану; ArduPilot зупиниться i триматиме позицiю. + void hold(); + + // Надiслати цiльову позицiю у локальнiй системi координат NED (метри вiд home). + // - north_m: додатне значення = пiвнiч + // - east_m: додатне значення = схiд + // - down_m: додатне значення = вниз (для наземного ровера лишити 0) + // На першому виклику автоматично стартує сесiя MAVSDK Offboard. + // Щоб команда спрацювала, FC має бути у режимi GUIDED. + void go_to_ned(float north_m, float east_m, float down_m = 0.0f); + +private: + struct Impl; + std::unique_ptr impl_; +}; diff --git a/homework_12/edge/c2/src/main.cpp b/homework_12/edge/c2/src/main.cpp new file mode 100644 index 0000000..f82d6f3 --- /dev/null +++ b/homework_12/edge/c2/src/main.cpp @@ -0,0 +1,36 @@ +#include "c2_controller.hpp" +#include +#include +#include +#include +#include +#include + +int main() { + nlohmann::json cfg; + { + std::ifstream f("/etc/c2/c2_config.json"); + if (!f.is_open()) { + std::cerr << "[C2] error: cannot open /etc/c2/c2_config.json\n"; + return 1; + } + try { + f >> cfg; + } catch (const nlohmann::json::exception& e) { + std::cerr << "[C2] error: invalid config: " << e.what() << "\n"; + return 1; + } + } + + const uint16_t fc_port = cfg.at("fc_port").get(); + + std::cout << "[C2] config: fc_port=" << fc_port << "\n"; + + C2Controller controller(fc_port); + + constexpr auto tick_interval = std::chrono::milliseconds(100); + while (true) { + controller.tick(); + std::this_thread::sleep_for(tick_interval); + } +} diff --git a/homework_12/edge/c2/src/udp_socket.hpp b/homework_12/edge/c2/src/udp_socket.hpp new file mode 100644 index 0000000..7851fde --- /dev/null +++ b/homework_12/edge/c2/src/udp_socket.hpp @@ -0,0 +1,70 @@ +#pragma once +#include +#include +#include +#include +#include + +#include +#include +#include + +// Неблокуючий UDP socket з RAII-володiнням ресурсом. +// У конструкторi робить bind на bind_port; при помилцi кидає std::runtime_error. +class UdpSocket { +public: + explicit UdpSocket(uint16_t bind_port) { + fd_ = ::socket(AF_INET, SOCK_DGRAM, 0); + if (fd_ < 0) + throw std::runtime_error("socket() failed"); + + ::fcntl(fd_, F_SETFL, O_NONBLOCK); + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_port = htons(bind_port); + + if (::bind(fd_, reinterpret_cast(&addr), sizeof(addr)) < 0) + throw std::runtime_error("bind() failed on port " + std::to_string(bind_port)); + } + + ~UdpSocket() { if (fd_ >= 0) ::close(fd_); } + + UdpSocket(const UdpSocket&) = delete; + UdpSocket& operator=(const UdpSocket&) = delete; + + UdpSocket(UdpSocket&& other) noexcept : fd_(other.fd_) { other.fd_ = -1; } + + // Прочитати данi у buf. Повертає кiлькiсть байтiв або -1, якщо даних немає (EAGAIN). + ssize_t recv(void* buf, std::size_t len) { + return ::recvfrom(fd_, buf, len, 0, nullptr, nullptr); + } + + // Прочитати данi разом з адресою sender, якщо потрiбно вiдповiдати назад. + ssize_t recv(void* buf, std::size_t len, sockaddr_in& sender) { + socklen_t slen = sizeof(sender); + return ::recvfrom(fd_, buf, len, 0, + reinterpret_cast(&sender), &slen); + } + + void send(const void* buf, std::size_t len, const sockaddr_in& to) { + ::sendto(fd_, buf, len, 0, + reinterpret_cast(&to), sizeof(to)); + } + + int fd() const { return fd_; } + +private: + int fd_ = -1; +}; + +// Створити sockaddr_in з IP-рядка у форматi a.b.c.d та порту. +inline sockaddr_in make_addr(const std::string& ip, uint16_t port) { + sockaddr_in a{}; + a.sin_family = AF_INET; + a.sin_port = htons(port); + if (::inet_pton(AF_INET, ip.c_str(), &a.sin_addr) != 1) + throw std::runtime_error("invalid IP: " + ip); + return a; +} diff --git a/homework_12/edge/config/c2_config.json b/homework_12/edge/config/c2_config.json new file mode 100644 index 0000000..953a0ea --- /dev/null +++ b/homework_12/edge/config/c2_config.json @@ -0,0 +1,3 @@ +{ + "fc_port": 14551 +} diff --git a/homework_12/edge/docker-compose.yml b/homework_12/edge/docker-compose.yml new file mode 100644 index 0000000..f340e54 --- /dev/null +++ b/homework_12/edge/docker-compose.yml @@ -0,0 +1,31 @@ +services: + c2_service: + # build: локальна збiрка для розробки та тестування ДЗ. + # На реальнiй цiльовiй машинi вихiдного коду немає, тому + # замiсть build: зазвичай використовується image: з готовим образом. + build: + context: ./c2 + dockerfile: Dockerfile + depends_on: + auto_stub: + condition: service_healthy + volumes: + - ./config/c2_config.json:/etc/c2/c2_config.json:ro # конфiг лише для читання + - ./logs:/var/log/c2 # логи видно на хостi пiсля stop + restart: _____ # для основного C2-сервiсу очiкується unless-stopped + network_mode: host + + auto_stub: + # Готовий stub для генерацiї точок маршруту. C2-сервiс має лише приймати + # його JSON-команди та вирiшувати, коли їх можна передавати в FC. + image: ghcr.io/yevhenkuznetsov/hw12-auto-stub:latest + environment: + C2_HOST: "127.0.0.1" + C2_PORT: "14560" + # auto_stub читає той самий маршрут, який можна вiдкрити в QGC. + ROUTE_PLAN: "/opt/route/lyman-patrol.plan" + # auto_stub не читає телеметрiю, тому перемикає точки маршруту за таймером. + WAYPOINT_INTERVAL_SEC: "45" + volumes: + - ../qgc:/opt/route:ro + network_mode: host diff --git a/homework_12/qgc/README.md b/homework_12/qgc/README.md new file mode 100644 index 0000000..4e88cc9 --- /dev/null +++ b/homework_12/qgc/README.md @@ -0,0 +1,54 @@ +# QGC-маршрут для патрулювання бiля Лимана + +Цей файл використовується у двох сценарiях: + +- маршрут QGC у режимi **Auto** для перевiрки SITL та руху ровера на картi; +- файл маршруту для готового `auto_stub` у C2-сценарiї режиму **Guided**. + +Маршрут можна змiнювати в QGC i зберiгати назад у цей `.plan` файл. Пiсля +змiни файлу маршруту перезапустити `auto_stub`, щоб вiн перечитав маршрут. + +## Як запустити + +1. Запустити SITL: +```bash +cd repository/homework_12 +docker compose -f sim/compose.sitl.yml up -d +``` + +2. Вiдкрити QGroundControl i дочекатися пiдключення до ровера. +3. Перейти в **Plan** view. +4. **File** -> **Open...** -> вибрати `qgc/lyman-patrol.plan`. +5. Натиснути **Upload**. +6. Перейти в **Fly** view. +7. Для перевiрки прямого маршруту QGC зупинити `c2_service` та `auto_stub`, якщо вони запущенi. +8. Перевести ровер у **Auto**, натиснути **Arm**, потiм **Start Mission**, якщо QGC показує окрему кнопку старту. + +## Як запустити через C2 / Guided + +1. Запустити SITL. +2. Запустити бортовий стек. +3. У QGC перевести ровер у **Guided**. +4. Натиснути **Arm**. +5. Дивитися логи: +```bash +cd repository/homework_12/edge +docker compose up -d --build +docker compose logs -f auto_stub c2_service +``` + +У цьому сценарiї **Start Mission** не натискати: маршрут читає готовий `auto_stub`, а C2 передає точки маршруту в FC тiльки у станi `ARMED_GUIDED`. + +`auto_stub` не читає телеметрiю з FC. Якщо перейти в **Manual**, поїхати джойстиком в iнше мiсце, а потiм повернутися в **Guided**, C2 має знову передавати поточну точку маршруту вiд `auto_stub`. Ровер може поїхати напряму до цiєї точки з поточної позицiї. Щоб почати маршрут з першої точки: + +```bash +docker compose restart auto_stub +``` + +## Що всерединi + +- 11 точок у районi Лимана. +- Тi самi точки у зворотному порядку. +- `DO_JUMP` на першу точку з repeat `-1`, щоб маршрут повторювався безкiнечно. + +QGC **Auto** сценарiй обходить C2: QGC завантажує mission прямо в ArduPilot. C2-сценарiй використовує той самий `.plan` файл, але маршрут читає готовий `auto_stub`. diff --git a/homework_12/qgc/lyman-patrol.plan b/homework_12/qgc/lyman-patrol.plan new file mode 100644 index 0000000..789de97 --- /dev/null +++ b/homework_12/qgc/lyman-patrol.plan @@ -0,0 +1,271 @@ +{ + "fileType": "Plan", + "geoFence": { + "circles": [], + "polygons": [], + "version": 2 + }, + "groundStation": "QGroundControl", + "mission": { + "cruiseSpeed": 3, + "firmwareType": 3, + "globalPlanAltitudeMode": 1, + "hoverSpeed": 5, + "items": [ + { + "AMSLAltAboveTerrain": null, + "Altitude": 0, + "AltitudeMode": 0, + "autoContinue": true, + "command": 16, + "doJumpId": 1, + "frame": 3, + "params": [0, 3, 0, null, 48.983417921213146, 37.823377137791915, 0], + "type": "SimpleItem" + }, + { + "AMSLAltAboveTerrain": null, + "Altitude": 0, + "AltitudeMode": 0, + "autoContinue": true, + "command": 16, + "doJumpId": 2, + "frame": 3, + "params": [0, 3, 0, null, 48.983486322862774, 37.82508972438823, 0], + "type": "SimpleItem" + }, + { + "AMSLAltAboveTerrain": null, + "Altitude": 0, + "AltitudeMode": 0, + "autoContinue": true, + "command": 16, + "doJumpId": 3, + "frame": 3, + "params": [0, 3, 0, null, 48.98355639155928, 37.828135540380586, 0], + "type": "SimpleItem" + }, + { + "AMSLAltAboveTerrain": null, + "Altitude": 0, + "AltitudeMode": 0, + "autoContinue": true, + "command": 16, + "doJumpId": 4, + "frame": 3, + "params": [0, 3, 0, null, 48.983658325819825, 37.83200091390991, 0], + "type": "SimpleItem" + }, + { + "AMSLAltAboveTerrain": null, + "Altitude": 0, + "AltitudeMode": 0, + "autoContinue": true, + "command": 16, + "doJumpId": 5, + "frame": 3, + "params": [0, 3, 0, null, 48.98374456936914, 37.83594776362669, 0], + "type": "SimpleItem" + }, + { + "AMSLAltAboveTerrain": null, + "Altitude": 0, + "AltitudeMode": 0, + "autoContinue": true, + "command": 16, + "doJumpId": 6, + "frame": 3, + "params": [0, 3, 0, null, 48.98385441385531, 37.83962726040233, 0], + "type": "SimpleItem" + }, + { + "AMSLAltAboveTerrain": null, + "Altitude": 0, + "AltitudeMode": 0, + "autoContinue": true, + "command": 16, + "doJumpId": 7, + "frame": 3, + "params": [0, 3, 0, null, 48.981173042786835, 37.839938860481915, 0], + "type": "SimpleItem" + }, + { + "AMSLAltAboveTerrain": null, + "Altitude": 0, + "AltitudeMode": 0, + "autoContinue": true, + "command": 16, + "doJumpId": 8, + "frame": 3, + "params": [0, 3, 0, null, 48.98109981882596, 37.84385660876785, 0], + "type": "SimpleItem" + }, + { + "AMSLAltAboveTerrain": null, + "Altitude": 0, + "AltitudeMode": 0, + "autoContinue": true, + "command": 16, + "doJumpId": 9, + "frame": 3, + "params": [0, 3, 0, null, 48.97956811428193, 37.84398593723947, 0], + "type": "SimpleItem" + }, + { + "AMSLAltAboveTerrain": null, + "Altitude": 0, + "AltitudeMode": 0, + "autoContinue": true, + "command": 16, + "doJumpId": 10, + "frame": 3, + "params": [0, 3, 0, null, 48.97945531059611, 37.84264276752271, 0], + "type": "SimpleItem" + }, + { + "AMSLAltAboveTerrain": null, + "Altitude": 0, + "AltitudeMode": 0, + "autoContinue": true, + "command": 16, + "doJumpId": 11, + "frame": 3, + "params": [0, 3, 0, null, 48.9786116936112, 37.8426168329051, 0], + "type": "SimpleItem" + }, + { + "AMSLAltAboveTerrain": null, + "Altitude": 0, + "AltitudeMode": 0, + "autoContinue": true, + "command": 16, + "doJumpId": 12, + "frame": 3, + "params": [0, 3, 0, null, 48.97945531059611, 37.84264276752271, 0], + "type": "SimpleItem" + }, + { + "AMSLAltAboveTerrain": null, + "Altitude": 0, + "AltitudeMode": 0, + "autoContinue": true, + "command": 16, + "doJumpId": 13, + "frame": 3, + "params": [0, 3, 0, null, 48.97956811428193, 37.84398593723947, 0], + "type": "SimpleItem" + }, + { + "AMSLAltAboveTerrain": null, + "Altitude": 0, + "AltitudeMode": 0, + "autoContinue": true, + "command": 16, + "doJumpId": 14, + "frame": 3, + "params": [0, 3, 0, null, 48.98109981882596, 37.84385660876785, 0], + "type": "SimpleItem" + }, + { + "AMSLAltAboveTerrain": null, + "Altitude": 0, + "AltitudeMode": 0, + "autoContinue": true, + "command": 16, + "doJumpId": 15, + "frame": 3, + "params": [0, 3, 0, null, 48.981173042786835, 37.839938860481915, 0], + "type": "SimpleItem" + }, + { + "AMSLAltAboveTerrain": null, + "Altitude": 0, + "AltitudeMode": 0, + "autoContinue": true, + "command": 16, + "doJumpId": 16, + "frame": 3, + "params": [0, 3, 0, null, 48.98385441385531, 37.83962726040233, 0], + "type": "SimpleItem" + }, + { + "AMSLAltAboveTerrain": null, + "Altitude": 0, + "AltitudeMode": 0, + "autoContinue": true, + "command": 16, + "doJumpId": 17, + "frame": 3, + "params": [0, 3, 0, null, 48.98374456936914, 37.83594776362669, 0], + "type": "SimpleItem" + }, + { + "AMSLAltAboveTerrain": null, + "Altitude": 0, + "AltitudeMode": 0, + "autoContinue": true, + "command": 16, + "doJumpId": 18, + "frame": 3, + "params": [0, 3, 0, null, 48.983658325819825, 37.83200091390991, 0], + "type": "SimpleItem" + }, + { + "AMSLAltAboveTerrain": null, + "Altitude": 0, + "AltitudeMode": 0, + "autoContinue": true, + "command": 16, + "doJumpId": 19, + "frame": 3, + "params": [0, 3, 0, null, 48.98355639155928, 37.828135540380586, 0], + "type": "SimpleItem" + }, + { + "AMSLAltAboveTerrain": null, + "Altitude": 0, + "AltitudeMode": 0, + "autoContinue": true, + "command": 16, + "doJumpId": 20, + "frame": 3, + "params": [0, 3, 0, null, 48.983486322862774, 37.82508972438823, 0], + "type": "SimpleItem" + }, + { + "AMSLAltAboveTerrain": null, + "Altitude": 0, + "AltitudeMode": 0, + "autoContinue": true, + "command": 16, + "doJumpId": 21, + "frame": 3, + "params": [0, 3, 0, null, 48.983417921213146, 37.823377137791915, 0], + "type": "SimpleItem" + }, + { + "AMSLAltAboveTerrain": null, + "Altitude": 0, + "AltitudeMode": 0, + "autoContinue": true, + "command": 177, + "doJumpId": 22, + "frame": 2, + "params": [1, -1, 0, 0, 0, 0, 0], + "type": "SimpleItem" + } + ], + "plannedHomePosition": [ + 48.983498065911704, + 37.82220625228142, + 95 + ], + "vehicleType": 10, + "version": 2 + }, + "rallyPoints": { + "points": [], + "version": 2 + }, + "version": 1 +} diff --git a/homework_12/sim/Dockerfile b/homework_12/sim/Dockerfile new file mode 100644 index 0000000..734548f --- /dev/null +++ b/homework_12/sim/Dockerfile @@ -0,0 +1,23 @@ +FROM ardupilot/ardupilot-dev-base:latest + +ARG ARDUPILOT_TAG=Rover-4.6.3 + +USER root + +# sim_vehicle.py використовує ip для перевiрки мережi та cleanup. +RUN apt-get update \ + && apt-get install -y --no-install-recommends iproute2 \ + && rm -rf /var/lib/apt/lists/* + +# Офiцiйний dev-base мiстить toolchain, але не мiстить source та готових binary. +RUN git clone --depth 1 --branch "${ARDUPILOT_TAG}" --recurse-submodules \ + https://github.com/ArduPilot/ardupilot.git /ardupilot \ + && cd /ardupilot \ + && ./waf configure --board sitl \ + && ./waf rover + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +WORKDIR /ardupilot +ENTRYPOINT ["/entrypoint.sh"] diff --git a/homework_12/sim/compose.sitl.yml b/homework_12/sim/compose.sitl.yml new file mode 100644 index 0000000..c56e781 --- /dev/null +++ b/homework_12/sim/compose.sitl.yml @@ -0,0 +1,24 @@ +services: + fc_sim: + build: . + container_name: fc_sim + environment: + VEHICLE: Rover + SPEEDUP: "1" + # За замовчуванням QGC слухає UDP 14550 на цьому ж хостi. + # Якщо QGC не пiдключається, спочатку лишити 127.0.0.1 i пересоздати контейнер. + # GCS_HOST з /etc/resolv.conf потрiбен тiльки для окремих WSL2 NAT-конфiгурацiй. + GCS_HOST: "${GCS_HOST:-127.0.0.1}" + GCS_PORT: "14550" + # C2 service працює в host network, тому 127.0.0.1 вказує на той самий network namespace. + C2_HOST: "127.0.0.1" + C2_PORT: "14551" + # Стартова позицiя SITL + LAT: "48.983498065911704" + LON: "37.82220625228142" + ALT: "95" + DIR: "90" + network_mode: host + # Docker Desktop на macOS не пiдтримує network_mode: host. + # Для macOS потрiбно прибрати network_mode та явно опублiкувати порти. + restart: unless-stopped diff --git a/homework_12/sim/entrypoint.sh b/homework_12/sim/entrypoint.sh new file mode 100644 index 0000000..a04b582 --- /dev/null +++ b/homework_12/sim/entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/sh +# --no-mavproxy: MAVLink виходить напряму з ArduPilot. +# У Rover-4.6.x використовуються --serial0/--serial1; старий --uartA не потрiбен. +# SERIAL0 -> QGroundControl +# SERIAL1 -> c2_service +exec /ardupilot/Tools/autotest/sim_vehicle.py \ + --vehicle "${VEHICLE:-Rover}" \ + --no-rebuild \ + --no-mavproxy \ + --speedup "${SPEEDUP:-1}" \ + --custom-location="${LAT:-48.983498065911704},${LON:-37.82220625228142},${ALT:-95},${DIR:-90}" \ + -A "--serial0=udpclient:${GCS_HOST:-127.0.0.1}:${GCS_PORT:-14550} --serial1=udpclient:${C2_HOST:-127.0.0.1}:${C2_PORT:-14551}" From 093241aa99fb16786e159123d9a2ba85ace4f5a0 Mon Sep 17 00:00:00 2001 From: blinker Date: Sat, 4 Jul 2026 12:28:11 +0000 Subject: [PATCH 2/6] Add homework_12 folder from course repo --- homework_12/edge/c2/src/c2_controller.cpp | 41 +++--- homework_12/edge/c2/src/c2_controller.hpp | 28 ++-- homework_12/edge/c2/src/fc_link.cpp | 170 ++++++++++++---------- homework_12/edge/c2/src/fc_link.hpp | 63 ++++---- homework_12/edge/c2/src/main.cpp | 46 +++--- homework_12/edge/c2/src/udp_socket.hpp | 86 ++++++----- 6 files changed, 234 insertions(+), 200 deletions(-) diff --git a/homework_12/edge/c2/src/c2_controller.cpp b/homework_12/edge/c2/src/c2_controller.cpp index 61ee898..5ccd171 100644 --- a/homework_12/edge/c2/src/c2_controller.cpp +++ b/homework_12/edge/c2/src/c2_controller.cpp @@ -11,33 +11,36 @@ static constexpr uint16_t STUB_PORT = 14560; struct C2Controller::Impl { - C2State state = C2State::DISARMED; - - // TODO: додати FcLink, UdpSocket, лог-файл та прапорцi стану. - // FcLink потребує fc_port у конструкторi Impl. - // UdpSocket має слухати STUB_PORT. - - void transition(C2State next) { - // TODO: якщо next != state, записати "PREV -> NEW" у stdout i лог, - // потiм оновити state. Якщо стан не змiнився, нiчого не писати. - (void)next; - } + C2State state = C2State::DISARMED; + + // TODO: додати FcLink, UdpSocket, лог-файл та прапорцi стану. + // FcLink потребує fc_port у конструкторi Impl. + // UdpSocket має слухати STUB_PORT. + + void transition(C2State next) + { + // TODO: якщо next != state, записати "PREV -> NEW" у stdout i лог, + // потiм оновити state. Якщо стан не змiнився, нiчого не писати. + (void)next; + } }; C2Controller::C2Controller(uint16_t fc_port) - : impl_(std::make_unique()) + : impl_(std::make_unique()) { - // TODO: передати fc_port в Impl та вiдкрити /var/log/c2/c2.log. - (void)fc_port; + // TODO: передати fc_port в Impl та вiдкрити /var/log/c2/c2.log. + (void)fc_port; } C2Controller::~C2Controller() = default; -void C2Controller::tick() { - // TODO: healthcheck, оновлення C2State, читання точки маршруту, - // передавання або блокування команди згiдно з поточним станом. +void C2Controller::tick() +{ + // TODO: healthcheck, оновлення C2State, читання точки маршруту, + // передавання або блокування команди згiдно з поточним станом. } -C2State C2Controller::current_state() const { - return impl_->state; +C2State C2Controller::current_state() const +{ + return impl_->state; } diff --git a/homework_12/edge/c2/src/c2_controller.hpp b/homework_12/edge/c2/src/c2_controller.hpp index f3f3c4f..91d35f3 100644 --- a/homework_12/edge/c2/src/c2_controller.hpp +++ b/homework_12/edge/c2/src/c2_controller.hpp @@ -4,10 +4,10 @@ #include enum class C2State { - DISARMED, - ARMED_HOLD, - ARMED_GUIDED, - ARMED_MANUAL, + DISARMED, + ARMED_HOLD, + ARMED_GUIDED, + ARMED_MANUAL, }; // C2-сервiс для арбiтражу команд. @@ -15,18 +15,18 @@ enum class C2State { // вiд auto_stub у JSON через UDP. Вирiшує, хто може командувати FC. class C2Controller { public: - explicit C2Controller(uint16_t fc_port); - ~C2Controller(); + explicit C2Controller(uint16_t fc_port); + ~C2Controller(); - // Обробити одну iтерацiю: передати або блокувати команди - // залежно вiд поточного стану. Викликати з main loop приблизно на 10 Hz. - void tick(); + // Обробити одну iтерацiю: передати або блокувати команди + // залежно вiд поточного стану. Викликати з main loop приблизно на 10 Hz. + void tick(); - // Read-only доступ до поточного стану C2. - // Корисно для перевiрок, тестiв або майбутньої дiагностики. - C2State current_state() const; + // Read-only доступ до поточного стану C2. + // Корисно для перевiрок, тестiв або майбутньої дiагностики. + C2State current_state() const; private: - struct Impl; - std::unique_ptr impl_; + struct Impl; + std::unique_ptr impl_; }; diff --git a/homework_12/edge/c2/src/fc_link.cpp b/homework_12/edge/c2/src/fc_link.cpp index 445368e..3c7fab0 100644 --- a/homework_12/edge/c2/src/fc_link.cpp +++ b/homework_12/edge/c2/src/fc_link.cpp @@ -13,92 +13,114 @@ #include struct FcLink::Impl { - mavsdk::Mavsdk mavsdk{mavsdk::Mavsdk::Configuration{mavsdk::Mavsdk::ComponentType::GroundStation}}; - std::shared_ptr system; - std::unique_ptr telemetry; - std::unique_ptr action; - std::unique_ptr offboard; - - std::atomic connected{false}; - std::atomic armed{false}; - std::atomic mode{FcLink::FlightMode::Unknown}; - - std::mutex offboard_mutex; - bool offboard_started = false; + mavsdk::Mavsdk mavsdk{mavsdk::Mavsdk::Configuration{mavsdk::Mavsdk::ComponentType::GroundStation}}; + std::shared_ptr system; + std::unique_ptr telemetry; + std::unique_ptr action; + std::unique_ptr offboard; + + std::atomic connected{false}; + std::atomic armed{false}; + std::atomic mode{FcLink::FlightMode::Unknown}; + + std::mutex offboard_mutex; + bool offboard_started = false; }; FcLink::FcLink(uint16_t listen_port, std::chrono::seconds timeout) - : impl_(std::make_unique()) + : impl_(std::make_unique()) { - std::string conn = "udp://:" + std::to_string(listen_port); - const auto connection_result = impl_->mavsdk.add_any_connection(conn); - if (connection_result != mavsdk::ConnectionResult::Success) { - throw std::runtime_error("FcLink: cannot open " + conn + ", result=" - + std::to_string(static_cast(connection_result))); - } - - std::promise> prom; - auto fut = prom.get_future(); - - impl_->mavsdk.subscribe_on_new_system([this, &prom]() { - for (auto& s : impl_->mavsdk.systems()) { - if (s->has_autopilot()) { - try { prom.set_value(s); } catch (...) {} - return; - } + std::string conn = "udp://:" + std::to_string(listen_port); + const auto connection_result = impl_->mavsdk.add_any_connection(conn); + if (connection_result != mavsdk::ConnectionResult::Success) { + throw std::runtime_error("FcLink: cannot open " + conn + ", result=" + std::to_string(static_cast(connection_result))); + } + + std::promise> prom; + auto fut = prom.get_future(); + + impl_->mavsdk.subscribe_on_new_system([this, &prom]() { + for (auto& s : impl_->mavsdk.systems()) { + if (s->has_autopilot()) { + try { + prom.set_value(s); } - }); - - if (fut.wait_for(timeout) != std::future_status::ready) - throw std::runtime_error("FcLink: timeout: is ArduPilot sending MAVLink to this UDP port?"); - - impl_->system = fut.get(); - impl_->telemetry = std::make_unique(impl_->system); - impl_->action = std::make_unique(impl_->system); - impl_->offboard = std::make_unique(impl_->system); - - impl_->telemetry->subscribe_armed([this](bool a) { - impl_->connected = true; - impl_->armed = a; - if (!a) impl_->mode = FlightMode::Unknown; - }); - - impl_->telemetry->subscribe_flight_mode([this](mavsdk::Telemetry::FlightMode m) { - using FM = mavsdk::Telemetry::FlightMode; - FlightMode mapped = FlightMode::Unknown; - switch (m) { - case FM::Manual: mapped = FlightMode::Manual; break; - case FM::Hold: mapped = FlightMode::Hold; break; - case FM::Offboard: mapped = FlightMode::Guided; break; - default: break; + catch (...) { } - impl_->mode = mapped; - }); + return; + } + } + }); + + if (fut.wait_for(timeout) != std::future_status::ready) + throw std::runtime_error("FcLink: timeout: is ArduPilot sending MAVLink to this UDP port?"); + + impl_->system = fut.get(); + impl_->telemetry = std::make_unique(impl_->system); + impl_->action = std::make_unique(impl_->system); + impl_->offboard = std::make_unique(impl_->system); + + impl_->telemetry->subscribe_armed([this](bool a) { + impl_->connected = true; + impl_->armed = a; + if (!a) + impl_->mode = FlightMode::Unknown; + }); + + impl_->telemetry->subscribe_flight_mode([this](mavsdk::Telemetry::FlightMode m) { + using FM = mavsdk::Telemetry::FlightMode; + FlightMode mapped = FlightMode::Unknown; + switch (m) { + case FM::Manual: + mapped = FlightMode::Manual; + break; + case FM::Hold: + mapped = FlightMode::Hold; + break; + case FM::Offboard: + mapped = FlightMode::Guided; + break; + default: + break; + } + impl_->mode = mapped; + }); } FcLink::~FcLink() = default; -bool FcLink::is_connected() const { return impl_->connected; } -bool FcLink::is_armed() const { return impl_->armed; } -FcLink::FlightMode FcLink::flight_mode() const { return impl_->mode; } +bool FcLink::is_connected() const +{ + return impl_->connected; +} +bool FcLink::is_armed() const +{ + return impl_->armed; +} +FcLink::FlightMode FcLink::flight_mode() const +{ + return impl_->mode; +} -void FcLink::hold() { - impl_->action->hold(); +void FcLink::hold() +{ + impl_->action->hold(); } -void FcLink::go_to_ned(float north_m, float east_m, float down_m) { - { - std::lock_guard lock(impl_->offboard_mutex); - if (!impl_->offboard_started) { - impl_->offboard->set_position_ned({}); - impl_->offboard->start(); - impl_->offboard_started = true; - } +void FcLink::go_to_ned(float north_m, float east_m, float down_m) +{ + { + std::lock_guard lock(impl_->offboard_mutex); + if (!impl_->offboard_started) { + impl_->offboard->set_position_ned({}); + impl_->offboard->start(); + impl_->offboard_started = true; } - mavsdk::Offboard::PositionNedYaw pos{}; - pos.north_m = north_m; - pos.east_m = east_m; - pos.down_m = down_m; - pos.yaw_deg = 0.0f; - impl_->offboard->set_position_ned(pos); + } + mavsdk::Offboard::PositionNedYaw pos{}; + pos.north_m = north_m; + pos.east_m = east_m; + pos.down_m = down_m; + pos.yaw_deg = 0.0f; + impl_->offboard->set_position_ned(pos); } diff --git a/homework_12/edge/c2/src/fc_link.hpp b/homework_12/edge/c2/src/fc_link.hpp index 3aa5e31..abfe729 100644 --- a/homework_12/edge/c2/src/fc_link.hpp +++ b/homework_12/edge/c2/src/fc_link.hpp @@ -32,46 +32,45 @@ // } class FcLink { public: - // Режим руху з ArduPilot HEARTBEAT. - // У ArduPilot це режим GUIDED. MAVSDK може показувати його як Offboard, - // тому FcLink нормалiзує це до FlightMode::Guided. - enum class FlightMode { Unknown, Manual, Hold, Guided }; + // Режим руху з ArduPilot HEARTBEAT. + // У ArduPilot це режим GUIDED. MAVSDK може показувати його як Offboard, + // тому FcLink нормалiзує це до FlightMode::Guided. + enum class FlightMode { Unknown, Manual, Hold, Guided }; - // Пiдключитися до ArduPilot. - // ArduPilot має надсилати MAVLink UDP-пакети на 127.0.0.1:. - // Блокується до вiдповiдi автопiлота або до завершення часу очiкування. - // Кидає std::runtime_error, якщо з'єднання не вiдкрилось або минув час очiкування. - explicit FcLink(uint16_t listen_port, - std::chrono::seconds timeout = std::chrono::seconds{30}); + // Пiдключитися до ArduPilot. + // ArduPilot має надсилати MAVLink UDP-пакети на 127.0.0.1:. + // Блокується до вiдповiдi автопiлота або до завершення часу очiкування. + // Кидає std::runtime_error, якщо з'єднання не вiдкрилось або минув час очiкування. + explicit FcLink(uint16_t listen_port, std::chrono::seconds timeout = std::chrono::seconds{30}); - ~FcLink(); + ~FcLink(); - FcLink(const FcLink&) = delete; - FcLink& operator=(const FcLink&) = delete; + FcLink(const FcLink&) = delete; + FcLink& operator=(const FcLink&) = delete; - // Повертає true пiсля першого отриманого HEARTBEAT. - // Використати це як сигнал для перевiрки стану (/tmp/c2_healthy). - bool is_connected() const; + // Повертає true пiсля першого отриманого HEARTBEAT. + // Використати це як сигнал для перевiрки стану (/tmp/c2_healthy). + bool is_connected() const; - // Повертає true, якщо FC зараз у станi armed. - bool is_armed() const; + // Повертає true, якщо FC зараз у станi armed. + bool is_armed() const; - // Повертає поточний режим руху. Оновлюється приблизно раз на 1 s через HEARTBEAT. - FlightMode flight_mode() const; + // Повертає поточний режим руху. Оновлюється приблизно раз на 1 s через HEARTBEAT. + FlightMode flight_mode() const; - // Надiслати MAVLink-команду для переходу FC у HOLD mode. - // Безпечно викликати з будь-якого стану; ArduPilot зупиниться i триматиме позицiю. - void hold(); + // Надiслати MAVLink-команду для переходу FC у HOLD mode. + // Безпечно викликати з будь-якого стану; ArduPilot зупиниться i триматиме позицiю. + void hold(); - // Надiслати цiльову позицiю у локальнiй системi координат NED (метри вiд home). - // - north_m: додатне значення = пiвнiч - // - east_m: додатне значення = схiд - // - down_m: додатне значення = вниз (для наземного ровера лишити 0) - // На першому виклику автоматично стартує сесiя MAVSDK Offboard. - // Щоб команда спрацювала, FC має бути у режимi GUIDED. - void go_to_ned(float north_m, float east_m, float down_m = 0.0f); + // Надiслати цiльову позицiю у локальнiй системi координат NED (метри вiд home). + // - north_m: додатне значення = пiвнiч + // - east_m: додатне значення = схiд + // - down_m: додатне значення = вниз (для наземного ровера лишити 0) + // На першому виклику автоматично стартує сесiя MAVSDK Offboard. + // Щоб команда спрацювала, FC має бути у режимi GUIDED. + void go_to_ned(float north_m, float east_m, float down_m = 0.0f); private: - struct Impl; - std::unique_ptr impl_; + struct Impl; + std::unique_ptr impl_; }; diff --git a/homework_12/edge/c2/src/main.cpp b/homework_12/edge/c2/src/main.cpp index f82d6f3..52886e3 100644 --- a/homework_12/edge/c2/src/main.cpp +++ b/homework_12/edge/c2/src/main.cpp @@ -6,31 +6,33 @@ #include #include -int main() { - nlohmann::json cfg; - { - std::ifstream f("/etc/c2/c2_config.json"); - if (!f.is_open()) { - std::cerr << "[C2] error: cannot open /etc/c2/c2_config.json\n"; - return 1; - } - try { - f >> cfg; - } catch (const nlohmann::json::exception& e) { - std::cerr << "[C2] error: invalid config: " << e.what() << "\n"; - return 1; - } +int main() +{ + nlohmann::json cfg; + { + std::ifstream f("/etc/c2/c2_config.json"); + if (!f.is_open()) { + std::cerr << "[C2] error: cannot open /etc/c2/c2_config.json\n"; + return 1; } + try { + f >> cfg; + } + catch (const nlohmann::json::exception& e) { + std::cerr << "[C2] error: invalid config: " << e.what() << "\n"; + return 1; + } + } - const uint16_t fc_port = cfg.at("fc_port").get(); + const uint16_t fc_port = cfg.at("fc_port").get(); - std::cout << "[C2] config: fc_port=" << fc_port << "\n"; + std::cout << "[C2] config: fc_port=" << fc_port << "\n"; - C2Controller controller(fc_port); + C2Controller controller(fc_port); - constexpr auto tick_interval = std::chrono::milliseconds(100); - while (true) { - controller.tick(); - std::this_thread::sleep_for(tick_interval); - } + constexpr auto tick_interval = std::chrono::milliseconds(100); + while (true) { + controller.tick(); + std::this_thread::sleep_for(tick_interval); + } } diff --git a/homework_12/edge/c2/src/udp_socket.hpp b/homework_12/edge/c2/src/udp_socket.hpp index 7851fde..f1bc684 100644 --- a/homework_12/edge/c2/src/udp_socket.hpp +++ b/homework_12/edge/c2/src/udp_socket.hpp @@ -13,58 +13,66 @@ // У конструкторi робить bind на bind_port; при помилцi кидає std::runtime_error. class UdpSocket { public: - explicit UdpSocket(uint16_t bind_port) { - fd_ = ::socket(AF_INET, SOCK_DGRAM, 0); - if (fd_ < 0) - throw std::runtime_error("socket() failed"); + explicit UdpSocket(uint16_t bind_port) + { + fd_ = ::socket(AF_INET, SOCK_DGRAM, 0); + if (fd_ < 0) + throw std::runtime_error("socket() failed"); - ::fcntl(fd_, F_SETFL, O_NONBLOCK); + ::fcntl(fd_, F_SETFL, O_NONBLOCK); - sockaddr_in addr{}; - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = INADDR_ANY; - addr.sin_port = htons(bind_port); + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_port = htons(bind_port); - if (::bind(fd_, reinterpret_cast(&addr), sizeof(addr)) < 0) - throw std::runtime_error("bind() failed on port " + std::to_string(bind_port)); - } + if (::bind(fd_, reinterpret_cast(&addr), sizeof(addr)) < 0) + throw std::runtime_error("bind() failed on port " + std::to_string(bind_port)); + } - ~UdpSocket() { if (fd_ >= 0) ::close(fd_); } + ~UdpSocket() + { + if (fd_ >= 0) + ::close(fd_); + } - UdpSocket(const UdpSocket&) = delete; - UdpSocket& operator=(const UdpSocket&) = delete; + UdpSocket(const UdpSocket&) = delete; + UdpSocket& operator=(const UdpSocket&) = delete; - UdpSocket(UdpSocket&& other) noexcept : fd_(other.fd_) { other.fd_ = -1; } + UdpSocket(UdpSocket&& other) noexcept + : fd_(other.fd_) + { + other.fd_ = -1; + } - // Прочитати данi у buf. Повертає кiлькiсть байтiв або -1, якщо даних немає (EAGAIN). - ssize_t recv(void* buf, std::size_t len) { - return ::recvfrom(fd_, buf, len, 0, nullptr, nullptr); - } + // Прочитати данi у buf. Повертає кiлькiсть байтiв або -1, якщо даних немає (EAGAIN). + ssize_t recv(void* buf, std::size_t len) { return ::recvfrom(fd_, buf, len, 0, nullptr, nullptr); } - // Прочитати данi разом з адресою sender, якщо потрiбно вiдповiдати назад. - ssize_t recv(void* buf, std::size_t len, sockaddr_in& sender) { - socklen_t slen = sizeof(sender); - return ::recvfrom(fd_, buf, len, 0, - reinterpret_cast(&sender), &slen); - } + // Прочитати данi разом з адресою sender, якщо потрiбно вiдповiдати назад. + ssize_t recv(void* buf, std::size_t len, sockaddr_in& sender) + { + socklen_t slen = sizeof(sender); + return ::recvfrom(fd_, buf, len, 0, reinterpret_cast(&sender), &slen); + } - void send(const void* buf, std::size_t len, const sockaddr_in& to) { - ::sendto(fd_, buf, len, 0, - reinterpret_cast(&to), sizeof(to)); - } + void send(const void* buf, std::size_t len, const sockaddr_in& to) + { + ::sendto(fd_, buf, len, 0, reinterpret_cast(&to), sizeof(to)); + } - int fd() const { return fd_; } + int fd() const { return fd_; } private: - int fd_ = -1; + int fd_ = -1; }; // Створити sockaddr_in з IP-рядка у форматi a.b.c.d та порту. -inline sockaddr_in make_addr(const std::string& ip, uint16_t port) { - sockaddr_in a{}; - a.sin_family = AF_INET; - a.sin_port = htons(port); - if (::inet_pton(AF_INET, ip.c_str(), &a.sin_addr) != 1) - throw std::runtime_error("invalid IP: " + ip); - return a; +inline sockaddr_in make_addr(const std::string& ip, uint16_t port) +{ + sockaddr_in a{}; + a.sin_family = AF_INET; + a.sin_port = htons(port); + if (::inet_pton(AF_INET, ip.c_str(), &a.sin_addr) != 1) + throw std::runtime_error("invalid IP: " + ip); + return a; } From 51b4709f22ee9e5cf7cfb54ae76902b9b6f5a6db Mon Sep 17 00:00:00 2001 From: blinker Date: Sat, 4 Jul 2026 19:59:38 +0000 Subject: [PATCH 3/6] [FEATURE] hw-12-docker-compose: implemented method tick of C2Controller, fill gaps in dockerfile and docker-compose.yaml --- homework_12/edge/c2/Dockerfile | 10 +- homework_12/edge/c2/src/c2_controller.cpp | 130 ++++++++++++++++++++-- homework_12/edge/c2/src/log.hpp | 17 +++ homework_12/edge/c2/tmp/c2_healthy | 0 homework_12/edge/docker-compose.yml | 2 +- 5 files changed, 143 insertions(+), 16 deletions(-) create mode 100644 homework_12/edge/c2/src/log.hpp create mode 100644 homework_12/edge/c2/tmp/c2_healthy diff --git a/homework_12/edge/c2/Dockerfile b/homework_12/edge/c2/Dockerfile index e4a2c28..aca2fbe 100644 --- a/homework_12/edge/c2/Dockerfile +++ b/homework_12/edge/c2/Dockerfile @@ -2,7 +2,7 @@ # MAVSDK у цьому ДЗ зафiксовано на конкретнiй версiї, щоб не шукати пакет вручну. ARG MAVSDK_VERSION=2.12.9 -FROM _____ AS builder +FROM ubuntu:22.04 AS builder ARG MAVSDK_VERSION ENV DEBIAN_FRONTEND=noninteractive @@ -21,12 +21,12 @@ RUN wget -qO /tmp/mavsdk-dev.deb \ WORKDIR /src COPY . . -# Заповнити команду CMake-збирання. -RUN _____ +# Заповнити команду CMake-збирання.///////////////////// +RUN cmake -S . -B build && cmake --build build # Етап 2: запуск -# Для простоти ДЗ той самий MAVSDK .deb встановлюється i в образ запуску. -FROM _____ +# Для простоти ДЗ той самий MAVSDK .deb встановлюється i в образ запуску.////////////////////// +FROM ubuntu:22.04 ARG MAVSDK_VERSION ENV DEBIAN_FRONTEND=noninteractive diff --git a/homework_12/edge/c2/src/c2_controller.cpp b/homework_12/edge/c2/src/c2_controller.cpp index 5ccd171..a242d93 100644 --- a/homework_12/edge/c2/src/c2_controller.cpp +++ b/homework_12/edge/c2/src/c2_controller.cpp @@ -1,14 +1,36 @@ #include "c2_controller.hpp" #include "fc_link.hpp" // MAVSDK обгортка, API описано у fc_link.hpp #include "udp_socket.hpp" // UDP прийом, API описано у udp_socket.hpp - +#include "log.hpp" #include // Розбiр JSON з точками маршруту вiд auto_stub #include #include #include +using json = nlohmann::json; + static constexpr uint16_t STUB_PORT = 14560; +static const std::string LOG_FILE_PATH = "/var/log/c2/c2.log"; + +static const std::string state_to_string(C2State state) +{ + switch (state) { + case C2State::DISARMED: { + return "DISARMED"; + } + case C2State::ARMED_HOLD: { + return "ARMED_HOLD"; + } + case C2State::ARMED_GUIDED: { + return "ARMED_GUIDED"; + } + case C2State::ARMED_MANUAL: { + return "ARMED_MANUAL"; + } + } + return "UNKNOWN"; +} struct C2Controller::Impl { C2State state = C2State::DISARMED; @@ -17,27 +39,115 @@ struct C2Controller::Impl { // FcLink потребує fc_port у конструкторi Impl. // UdpSocket має слухати STUB_PORT. + FcLink fc; + UdpSocket updSocket; + std::ofstream log_file; + bool healthy_written = false; + + Impl(uint16_t fc_port) + : fc(fc_port) + , updSocket(STUB_PORT) + { + log_file.open(LOG_FILE_PATH, std::ios::app); + }; + void transition(C2State next) { - // TODO: якщо next != state, записати "PREV -> NEW" у stdout i лог, - // потiм оновити state. Якщо стан не змiнився, нiчого не писати. - (void)next; + if (next == state) { + return; + } + + log(std::string("[C2] state: ") + state_to_string(state) + " -> " + state_to_string(next)); + state = next; + } + + void log(const std::string& message) + { + LOG(message); + + if (!log_file.is_open()) { + return; + } + log_file << message << std::endl; + } + + void close_log_file() + { + if (log_file.is_open()) { + log_file.close(); + } } }; C2Controller::C2Controller(uint16_t fc_port) - : impl_(std::make_unique()) + : impl_(std::make_unique(fc_port)) { - // TODO: передати fc_port в Impl та вiдкрити /var/log/c2/c2.log. - (void)fc_port; } -C2Controller::~C2Controller() = default; +C2Controller::~C2Controller() +{ + this->impl_->close_log_file(); +}; void C2Controller::tick() { - // TODO: healthcheck, оновлення C2State, читання точки маршруту, - // передавання або блокування команди згiдно з поточним станом. + if (!this->impl_->healthy_written && this->impl_->fc.is_connected()) { + std::ofstream("/tmp/c2_healthy").close(); + this->impl_->healthy_written = true; + } + + C2State state = C2State::DISARMED; + if (this->impl_->fc.is_armed() == true) { + switch (this->impl_->fc.flight_mode()) { + case FcLink::FlightMode::Unknown: + return; + + case FcLink::FlightMode::Manual: + // не заважати ручному керуванню + state = C2State::ARMED_MANUAL; + break; + + case FcLink::FlightMode::Hold: + // один раз на вхiд у стан надiслати fc.hold() + state = C2State::ARMED_HOLD; + break; + + case FcLink::FlightMode::Guided: + // передавати + // розбирати JSON i викликати fc.go_to_ned(north, east) + state = C2State::ARMED_GUIDED; + break; + } + } + + const bool entering_hold = (state == C2State::ARMED_HOLD && this->impl_->state != C2State::ARMED_HOLD); + this->impl_->transition(state); + if (entering_hold) { + this->impl_->fc.hold(); + } + + char buf[1024]; + ssize_t n = this->impl_->updSocket.recv(buf, sizeof(buf) - 1); + if (n <= 0) { + return; + } + buf[n] = '\0'; + + if (this->impl_->state == C2State::ARMED_GUIDED) { + try { + auto waypoint = json::parse(buf); + float north = waypoint.at("north_m").get(); + float east = waypoint.at("east_m").get(); + this->impl_->fc.go_to_ned(north, east); + this->impl_->log("[C2] fwd: north=" + std::to_string(north) + " east=" + std::to_string(east)); + } + catch (const std::exception& e) { + DEBUG("Error: waypoint failed " << e.what()); + } + } + else { + this->impl_->log(std::string("[C2] blocked: waypoint in ") + state_to_string(this->impl_->state)); + } } C2State C2Controller::current_state() const diff --git a/homework_12/edge/c2/src/log.hpp b/homework_12/edge/c2/src/log.hpp new file mode 100644 index 0000000..e3ed2eb --- /dev/null +++ b/homework_12/edge/c2/src/log.hpp @@ -0,0 +1,17 @@ +#pragma once +#include + +#define ENABLE_LOG 1 +#define ENABLE_DEBUG 1 + +#if ENABLE_LOG +#define LOG(msg) std::cout << msg << std::endl +#else +#define LOG(msg) +#endif + +#if ENABLE_DEBUG +#define DEBUG(msg) std::cout << "[DEBUG] " << msg << std::endl +#else +#define DEBUG(msg) +#endif \ No newline at end of file diff --git a/homework_12/edge/c2/tmp/c2_healthy b/homework_12/edge/c2/tmp/c2_healthy new file mode 100644 index 0000000..e69de29 diff --git a/homework_12/edge/docker-compose.yml b/homework_12/edge/docker-compose.yml index f340e54..37f90d8 100644 --- a/homework_12/edge/docker-compose.yml +++ b/homework_12/edge/docker-compose.yml @@ -12,7 +12,7 @@ services: volumes: - ./config/c2_config.json:/etc/c2/c2_config.json:ro # конфiг лише для читання - ./logs:/var/log/c2 # логи видно на хостi пiсля stop - restart: _____ # для основного C2-сервiсу очiкується unless-stopped + restart: unless-stopped # для основного C2-сервiсу очiкується unless-stopped network_mode: host auto_stub: From 83d14192de63bdbd3386c101bc21244780714418 Mon Sep 17 00:00:00 2001 From: alex-popkov Date: Wed, 22 Jul 2026 22:48:16 +0300 Subject: [PATCH 4/6] Apply suggestions from code review Co-authored-by: Eugene Kuznetsov --- .devcontainer/Dockerfile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 0a7786b..efb5831 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -18,7 +18,11 @@ FROM ubuntu:noble@sha256:c4a8d5503dfb2a3eb8ab5f807da5bc69a85730fb49b5cfca2330194 ARG DEBIAN_FRONTEND="noninteractive" -RUN dpkg --add-architecture arm64 && apt update +COPY ubuntu-amd64.sources /etc/apt/sources.list.d/ubuntu.sources +COPY ubuntu-arm64.sources /etc/apt/sources.list.d/ubuntu-arm64.sources +RUN dpkg --add-architecture arm64 && \ + apt-get update && \ + apt-get install --no-install-recommends -y libgpiod-dev:arm64 RUN apt install --no-install-recommends -y libgpiod-dev:arm64 # Пакети зі списку apt-packages.in (по одному на рядок, коментарі через #). # From 5d7b0ab4bfb7fa16487e59a0e1ec873759ce54be Mon Sep 17 00:00:00 2001 From: blinker Date: Sat, 15 Aug 2026 09:44:48 +0000 Subject: [PATCH 5/6] [FIX] fix devcontainer. added separate sources for different arch --- .devcontainer/Dockerfile | 5 ++++- .devcontainer/ubuntu-amd64.sources | 16 ++++++++++++++++ .devcontainer/ubuntu-arm64.sources | 9 +++++++++ .idea/.gitignore | 10 ++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 .devcontainer/ubuntu-amd64.sources create mode 100644 .devcontainer/ubuntu-arm64.sources create mode 100644 .idea/.gitignore diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index efb5831..2975876 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -18,12 +18,15 @@ FROM ubuntu:noble@sha256:c4a8d5503dfb2a3eb8ab5f807da5bc69a85730fb49b5cfca2330194 ARG DEBIAN_FRONTEND="noninteractive" +# Розділені deb822-джерела: amd64 з archive.ubuntu.com, arm64 з +# ports.ubuntu.com. Обидва файли обов'язкові в build context, інакше +# COPY провалить збірку. Без arch-обмежень apt тягнув би arm64 з +# archive.ubuntu.com (там його немає -> 404). COPY ubuntu-amd64.sources /etc/apt/sources.list.d/ubuntu.sources COPY ubuntu-arm64.sources /etc/apt/sources.list.d/ubuntu-arm64.sources RUN dpkg --add-architecture arm64 && \ apt-get update && \ apt-get install --no-install-recommends -y libgpiod-dev:arm64 -RUN apt install --no-install-recommends -y libgpiod-dev:arm64 # Пакети зі списку apt-packages.in (по одному на рядок, коментарі через #). # # Прапори й чистка: diff --git a/.devcontainer/ubuntu-amd64.sources b/.devcontainer/ubuntu-amd64.sources new file mode 100644 index 0000000..c23c8b0 --- /dev/null +++ b/.devcontainer/ubuntu-amd64.sources @@ -0,0 +1,16 @@ +# amd64 пакети беремо з основного дзеркала archive.ubuntu.com. +# Architectures: amd64 обмежує ці репозиторії лише host-архітектурою, +# щоб apt НЕ намагався тягнути arm64 звідси (тут його немає -> 404). +Types: deb +URIs: http://archive.ubuntu.com/ubuntu/ +Suites: noble noble-updates noble-backports +Components: main restricted universe multiverse +Architectures: amd64 +Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg + +Types: deb +URIs: http://security.ubuntu.com/ubuntu/ +Suites: noble-security +Components: main restricted universe multiverse +Architectures: amd64 +Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg diff --git a/.devcontainer/ubuntu-arm64.sources b/.devcontainer/ubuntu-arm64.sources new file mode 100644 index 0000000..56079ae --- /dev/null +++ b/.devcontainer/ubuntu-arm64.sources @@ -0,0 +1,9 @@ +# arm64 пакети (для крос-компіляції під RPi4/ROCK 5B/Jetson) є лише на +# ports.ubuntu.com, а не на archive.ubuntu.com. Architectures: arm64 +# обмежує це дзеркало лише arm64, щоб воно не заважало amd64-джерелам. +Types: deb +URIs: http://ports.ubuntu.com/ubuntu-ports/ +Suites: noble noble-updates noble-backports noble-security +Components: main restricted universe multiverse +Architectures: arm64 +Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml From cd66f4765836b451afec9938aa421e3d61820218 Mon Sep 17 00:00:00 2001 From: blinker Date: Sat, 15 Aug 2026 10:01:25 +0000 Subject: [PATCH 6/6] [FIX] devcontainer: add arch-split apt sources, drop duplicate arm64 install --- .devcontainer/Dockerfile | 1 + .devcontainer/ubuntu-amd64.sources | 13 ++++++------- .devcontainer/ubuntu-arm64.sources | 8 +++----- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 2975876..16a35bd 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -24,6 +24,7 @@ ARG DEBIAN_FRONTEND="noninteractive" # archive.ubuntu.com (там його немає -> 404). COPY ubuntu-amd64.sources /etc/apt/sources.list.d/ubuntu.sources COPY ubuntu-arm64.sources /etc/apt/sources.list.d/ubuntu-arm64.sources + RUN dpkg --add-architecture arm64 && \ apt-get update && \ apt-get install --no-install-recommends -y libgpiod-dev:arm64 diff --git a/.devcontainer/ubuntu-amd64.sources b/.devcontainer/ubuntu-amd64.sources index c23c8b0..69c7771 100644 --- a/.devcontainer/ubuntu-amd64.sources +++ b/.devcontainer/ubuntu-amd64.sources @@ -1,16 +1,15 @@ -# amd64 пакети беремо з основного дзеркала archive.ubuntu.com. -# Architectures: amd64 обмежує ці репозиторії лише host-архітектурою, -# щоб apt НЕ намагався тягнути arm64 звідси (тут його немає -> 404). +# Стандартні Ubuntu-репозиторії для нативних amd64 пакетів devcontainer. Types: deb -URIs: http://archive.ubuntu.com/ubuntu/ +URIs: http://archive.ubuntu.com/ubuntu Suites: noble noble-updates noble-backports -Components: main restricted universe multiverse +Components: main universe restricted multiverse Architectures: amd64 Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg +# Security updates винесено окремо, бо вони публікуються на іншому mirror. Types: deb -URIs: http://security.ubuntu.com/ubuntu/ +URIs: http://security.ubuntu.com/ubuntu Suites: noble-security -Components: main restricted universe multiverse +Components: main universe restricted multiverse Architectures: amd64 Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg diff --git a/.devcontainer/ubuntu-arm64.sources b/.devcontainer/ubuntu-arm64.sources index 56079ae..d6edd5e 100644 --- a/.devcontainer/ubuntu-arm64.sources +++ b/.devcontainer/ubuntu-arm64.sources @@ -1,9 +1,7 @@ -# arm64 пакети (для крос-компіляції під RPi4/ROCK 5B/Jetson) є лише на -# ports.ubuntu.com, а не на archive.ubuntu.com. Architectures: arm64 -# обмежує це дзеркало лише arm64, щоб воно не заважало amd64-джерелам. +# ARM64 пакети Ubuntu публікуються через ports.ubuntu.com. Types: deb -URIs: http://ports.ubuntu.com/ubuntu-ports/ +URIs: http://ports.ubuntu.com/ubuntu-ports Suites: noble noble-updates noble-backports noble-security -Components: main restricted universe multiverse +Components: main universe restricted multiverse Architectures: arm64 Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg