diff --git a/.idea/vcs.xml b/.idea/vcs.xml index d23aa41..35eb1dd 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,7 +2,5 @@ - - \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d216cc..b6f463f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,11 +6,51 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(OpenGL_GL_PREFERENCE GLVND) -# Define file paths for thirdparty modules -set(IMGUI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty/imgui) -set(IMPLOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty/implot) +# ============================================================================== +# ЗАГРУЗКА ЗАВИСИМОСТЕЙ ЧЕРЕЗ FETCHCONTENT (ImGui и ImPlot) +# ============================================================================== +include(FetchContent) -# Find Required Packages +# 1. Загрузка Dear ImGui +FetchContent_Declare( + imgui + GIT_REPOSITORY https://github.com/ocornut/imgui.git + GIT_TAG master +) +FetchContent_MakeAvailable(imgui) + +# 2. Загрузка ImPlot +FetchContent_Declare( + implot + GIT_REPOSITORY https://github.com/epezent/implot.git + GIT_TAG master +) +FetchContent_MakeAvailable(implot) + +# ============================================================================== +# НАСТРОЙКА ИСХОДНЫХ КОДОВ БИБЛИОТЕК +# ============================================================================== +# Исходники ImGui +set(IMGUI_SOURCES + ${imgui_SOURCE_DIR}/imgui.cpp + ${imgui_SOURCE_DIR}/imgui_demo.cpp + ${imgui_SOURCE_DIR}/imgui_draw.cpp + ${imgui_SOURCE_DIR}/imgui_tables.cpp + ${imgui_SOURCE_DIR}/imgui_widgets.cpp + ${imgui_SOURCE_DIR}/backends/imgui_impl_glfw.cpp + ${imgui_SOURCE_DIR}/backends/imgui_impl_opengl3.cpp +) + +# Исходники ImPlot +set(IMPLOT_SOURCES + ${implot_SOURCE_DIR}/implot.cpp + ${implot_SOURCE_DIR}/implot_items.cpp + ${implot_SOURCE_DIR}/implot_demo.cpp +) + +# ============================================================================== +# ПОИСК СИСТЕМНЫХ ПАКЕТОВ +# ============================================================================== find_package(PkgConfig REQUIRED) find_package(OpenGL REQUIRED) pkg_check_modules(GLFW REQUIRED glfw3) @@ -18,25 +58,9 @@ pkg_check_modules(LIBSERIALPORT REQUIRED IMPORTED_TARGET libserialport) find_package(Protobuf REQUIRED) find_package(gRPC REQUIRED) -# ImGUI things -set(IMGUI_SOURCES - ${IMGUI_DIR}/imgui.cpp - ${IMGUI_DIR}/imgui_demo.cpp - ${IMGUI_DIR}/imgui_draw.cpp - ${IMGUI_DIR}/imgui_tables.cpp - ${IMGUI_DIR}/imgui_widgets.cpp - ${IMGUI_DIR}/backends/imgui_impl_glfw.cpp - ${IMGUI_DIR}/backends/imgui_impl_opengl3.cpp -) - -set(IMPLOT_SOURCES - ${IMPLOT_DIR}/implot.cpp - ${IMPLOT_DIR}/implot_items.cpp - ${IMPLOT_DIR}/implot_demo.cpp - sources/SerialApp.cpp - include/SerialApp.h -) - +# ============================================================================== +# ГЕНЕРАЦИЯ PROTOBUF & GRPC +# ============================================================================== set(proto_srcs "${CMAKE_CURRENT_BINARY_DIR}/service.pb.cc") set(proto_hdrs "${CMAKE_CURRENT_BINARY_DIR}/service.pb.h") set(grpc_srcs "${CMAKE_CURRENT_BINARY_DIR}/service.grpc.pb.cc") @@ -65,9 +89,14 @@ target_include_directories(proto_lib PUBLIC ${Protobuf_INCLUDE_DIRS} ) +# ============================================================================== +# СБОРКА ОСНОВНОГО ПРИЛОЖЕНИЯ +# ============================================================================== add_executable(${PROJECT_NAME} main.cpp ${IMGUI_SOURCES} ${IMPLOT_SOURCES} + sources/SerialApp.cpp + include/SerialApp.h sources/ConnectionWindow.cpp include/ConnectionWindow.h sources/PayloadWindow.cpp @@ -80,9 +109,9 @@ target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_BINARY_DIR} ${Protobuf_INCLUDE_DIRS} - ${IMGUI_DIR} - ${IMGUI_DIR}/backends - ${IMPLOT_DIR} + ${imgui_SOURCE_DIR} + ${imgui_SOURCE_DIR}/backends + ${implot_SOURCE_DIR} ${GLFW_INCLUDE_DIRS} ${OPENGL_INCLUDE_DIR} ) @@ -98,3 +127,34 @@ target_link_libraries(${PROJECT_NAME} PRIVATE ) configure_file(style/dracula.theme ${CMAKE_CURRENT_BINARY_DIR}/dracula.theme COPYONLY) + + +#Является частой практикой в больших проектах для поддержки +#единой схемы данныхмежду сервером и клиентом. +#Если .proto лежит в отдельном Git-репозитории на сервере +#лучше использовать FetchContent. Он корректно кэширует файлы +#и обновляет их только при смене тега или хэша коммита. +#``` +#FetchContent_Declare( +#remote_proto +#GIT_REPOSITORY git@your-server.com:shared/protocols.git # URL вашего репозитория +#GIT_TAG main # Ветка, тег или хэш коммита +#) +#FetchContent_MakeAvailable(remote_proto) +#``` +#Теперь файл доступен по пути: ${remote_proto_SOURCE_DIR}/service.proto +# +#не забыть обновить команду компиляции Protobuf, +#заменив локальные пути на пути из FetchContent: +#``` +#add_custom_command( +#OUTPUT "${proto_srcs}" "${proto_hdrs}" "${grpc_srcs}" "${grpc_hdrs}" +#COMMAND ${Protobuf_PROTOC_EXECUTABLE} +#ARGS --grpc_out="${CMAKE_CURRENT_BINARY_DIR}" +#--cpp_out="${CMAKE_CURRENT_BINARY_DIR}" +#-I "${remote_proto_SOURCE_DIR}" # Корневая папка для импортов +#--plugin=protoc-gen-grpc=/usr/bin/grpc_cpp_plugin +#"${REMOTE_PROTO_PATH}" # Сам файл +#DEPENDS "${REMOTE_PROTO_PATH}" +#) +#``` diff --git a/main.cpp b/main.cpp index c2dac3d..a019093 100644 --- a/main.cpp +++ b/main.cpp @@ -17,54 +17,67 @@ static void glfw_error_callback(int error, const char* description) { fprintf(stderr, "GLFW Error %d: %s\n", error, description); } -// Простейшая реализация вашего сервиса gRPC (для примера) +bool initImGUI(GLFWwindow *&window) { + glfwSetErrorCallback(glfw_error_callback); + if (!glfwInit()) { + return true; + } + + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + + window = glfwCreateWindow(1280, 720, "Serial App", nullptr, nullptr); + if (window == nullptr) { + return true; + } + glfwMakeContextCurrent(window); + glfwSwapInterval(1); + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + const ImGuiIO& io = ImGui::GetIO(); (void)io; + ImGui::StyleColorsDark(); + + ImGui_ImplGlfw_InitForOpenGL(window, true); + ImGui_ImplOpenGL3_Init("#version 130"); + return false; +} + +// Простейшая реализация сервиса gRPC class MyServiceImpl final : public serial_sample::Greeter::Service { grpc::Status SayHello(grpc::ServerContext* context, const serial_sample::HelloRequest* request, serial_sample::HelloReply* reply) override { + std::cout << request->name() << std::endl; reply->set_message("Hello, " + request->name() + "!"); return grpc::Status::OK; } }; int main(int, char**) { - glfwSetErrorCallback(glfw_error_callback); - if (!glfwInit()) return 1; + GLFWwindow *window; + if (initImGUI(window)) { + return 1; + } - glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); - glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); - glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); - - GLFWwindow* window = glfwCreateWindow(1280, 720, "Serial App", nullptr, nullptr); - if (window == nullptr) return 1; - glfwMakeContextCurrent(window); - glfwSwapInterval(1); - - IMGUI_CHECKVERSION(); - ImGui::CreateContext(); - ImGuiIO& io = ImGui::GetIO(); (void)io; - ImGui::StyleColorsDark(); - - ImGui_ImplGlfw_InitForOpenGL(window, true); - ImGui_ImplOpenGL3_Init("#version 130"); - - SerialApp app; + SerialApp app; // frontend manager app.initialize(); - // === НАЧАЛО ИНИЦИАЛИЗАЦИИ gRPC СЕРВЕРА === - std::string server_address("0.0.0.0:50051"); - MyServiceImpl grpc_service; + // инициализация gRPC + const std::string serverAddress("0.0.0.0:50051"); + MyServiceImpl serviceImpl; grpc::ServerBuilder builder; - builder.AddListeningPort(server_address, grpc::InsecureServerCredentials()); - builder.RegisterService(&grpc_service); + builder.AddListeningPort(serverAddress, grpc::InsecureServerCredentials()); + builder.RegisterService(&serviceImpl); - std::unique_ptr grpc_server(builder.BuildAndStart()); - std::cout << "gRPC Server listening on " << server_address << std::endl; + std::unique_ptr gRPCServer(builder.BuildAndStart()); + std::cout << "gRPC Server listening on " << serverAddress << std::endl; - // Запускаем сервер в фоновом потоке - std::thread grpc_thread([&grpc_server]() { - grpc_server->Wait(); // Блокирует фоновый поток, пока сервер работает + // сервер стартует в фоновом БЛОКИРУЮЩЕМ потоке + std::thread gRPCThread([&gRPCServer] { + gRPCServer->Wait(); }); // ========================================= @@ -82,9 +95,9 @@ int main(int, char**) { } ImGui::Render(); - int display_w, display_h; - glfwGetFramebufferSize(window, &display_w, &display_h); - glViewport(0, 0, display_w, display_h); + int width, height; + glfwGetFramebufferSize(window, &width, &height); + glViewport(0, 0, width, height); glClearColor(0.45f, 0.55f, 0.60f, 1.00f); glClear(GL_COLOR_BUFFER_BIT); @@ -92,14 +105,14 @@ int main(int, char**) { glfwSwapBuffers(window); } - // === КОРРЕКТНОЕ ВЫКЛЮЧЕНИЕ gRPC СЕРВЕРА === + // выключение gRPC std::cout << "Shutting down gRPC server..." << std::endl; // Останавливаем сервер (это разблокирует метод Wait() в фоновом потоке) - grpc_server->Shutdown(); + gRPCServer->Shutdown(); // Обязательно дожидаемся завершения фонового потока перед выходом из main - if (grpc_thread.joinable()) { - grpc_thread.join(); + if (gRPCThread.joinable()) { + gRPCThread.join(); } std::cout << "gRPC server thread joined." << std::endl; // ========================================= diff --git a/proto/serialCtrl.proto b/proto/serialCtrl.proto new file mode 100644 index 0000000..aeb1a96 --- /dev/null +++ b/proto/serialCtrl.proto @@ -0,0 +1,305 @@ +/** + * @file serial.proto + * @brief Протокол обмена между GUI‑клиентом и backend‑службой + * для работы с последовательным (UART/RS‑232/USB‑CDC) портом. + * + * Протокол построен на базе **Protocol Buffers 3** и **gRPC**. + * Он покрывает все типичные задачи: + * - перечисление доступных портов; + * - открытие/закрытие порта с полным набором параметров; + * - отправку и приём произвольных пакетов; + * - ведение журнала (лог‑стрим) всех переданных/полученных байтов + * – удобно для отладки и верификации протокола. + * + * Файл снабжён Doxygen‑комментариями, поэтому из него можно + * автоматически получить красивую HTML‑/PDF‑документацию. + */ +syntax = "proto3"; + +package serial; + +/* -------------------------------------------------------------------------- */ +/* Enums – вспомогательные типы */ +/* -------------------------------------------------------------------------- */ + +/** + * @enum Parity + * @brief Возможные варианты чётности (parity) при работе с UART. + * + * Имена полностью совпадают с теми, что использует библиотека **pyserial** + * (и, в принципе, большинство C/C++‑библиотек для последовательных портов). + */ +enum Parity { + PARITY_NONE = 0; /// Без чётности. + PARITY_EVEN = 1; /// Чётность чётных битов. + PARITY_ODD = 2; /// Чётность нечётных битов. + PARITY_MARK = 3; /// Маркер (всегда 1). + PARITY_SPACE= 4; /// Пробел (всегда 0). +} + +/** + * @enum StopBits + * @brief Конфигурация стоп‑битов. + * + * Значения соответствуют типичным настройкам UART‑контроллера. + */ +enum StopBits { + STOPBITS_ONE = 0; /// Один стоп‑бит. + STOPBITS_ONE_POINT_FIVE = 1; /// Полтора стоп‑бита (реже используется). + STOPBITS_TWO = 2; /// Два стоп‑бита. +} + +/** + * @enum StatusCode + * @brief Универсальный набор кодов возврата для большинства RPC‑методов. + * + * При необходимости можно добавить новые коды, но текущего набора достаточно + * для базовых сценариев. + */ +enum StatusCode { + OK = 0; /// Операция выполнена успешно. + INVALID_ARGUMENT = 1; /// Переданы некорректные аргументы (например, отрицательная скорость). + NOT_FOUND = 2; /// Запрашиваемый объект (порт, файл и т.п.) не найден. + INTERNAL_ERROR = 3; /// Внутренняя ошибка сервера (исключение, недоступный ресурс). + NOT_CONNECTED = 4; /// Операция невозможна, т.к. порт не открыт. +} + +/* -------------------------------------------------------------------------- */ +/* Сообщения – описание данных, передаваемых по gRPC */ +/* -------------------------------------------------------------------------- */ + +/** + * @message SerialDevice + * @brief Информация об одном последовательном порте, доступном в системе. + * + * @param port_name Системное имя порта (например, «COM3», «/dev/ttyUSB0»). + * @param description Человекочитаемое описание, если ОС его предоставляет. + * @param hardware_id Строка‑идентификатор устройства (VID/PID и т.п.). + */ +message SerialDevice { + string port_name = 1; + string description = 2; + string hardware_id = 3; +} + +/** + * @message SerialConfig + * @brief Полный набор параметров, необходимый для открытия последовательного порта. + * + * @param port_name Должно совпадать с `SerialDevice.port_name`. + * @param baud_rate Скорость в бодах (например, 9600, 115200 …). + * @param data_bits Количество битов данных (5‑8, обычно 8). + * @param parity Чётность (см. enum @ref Parity). + * @param stop_bits Стоп‑биты (см. enum @ref StopBits). + * @param packet_size Фиксированный размер полезной нагрузки пакета. + * 0 → размер произвольный (по умолчанию). + */ +message SerialConfig { + string port_name = 1; + uint32 baud_rate = 2; + uint32 data_bits = 3; + Parity parity = 4; + StopBits stop_bits = 5; + uint32 packet_size = 6; +} + +/** + * @message ConfigResponse + * @brief Ответ на запрос `Open`/`Close`. Содержит статус операции и, + * при ошибке, человекочитаемое сообщение. + * + * @param status Код статуса (см. enum @ref StatusCode). + * @param error Текстовое пояснение, заполнено только если `status != OK`. + */ +message ConfigResponse { + StatusCode status = 1; + string error = 2; +} + +/** + * @message Packet + * @brief Описание логической "пакетной" единицы, которую GUI отправляет + * на backend, а backend – в последовательный порт. + * + * Полезная нагрузка (`payload`) передаётся «как есть» (raw‑bytes). + * + * @param seq_id Последовательный номер пакета, генерируется клиентом. + * Удобен для отладки и согласования запрос‑ответ. + * @param payload Бинарные данные, которые действительно окажутся на линии. + * @param tags Необязательная карта «ключ → значение», используемая + * только внутри программы (не попадает в кадр UART). + */ +message Packet { + uint64 seq_id = 1; + bytes payload = 2; + map tags = 3; +} + +/** + * @message SendResponse + * @brief Ответ на RPC `SendPacket`. Содержит статус, сообщение об ошибке и + * «эхо‑идентификатор», позволяющий клиенту убедиться, что ответ + * относится к конкретному запросу. + * + * @param status Код статуса (см. enum @ref StatusCode). + * @param error Текстовое описание ошибки (если есть). + * @param echo_seq_id Идентификатор пакета, пришедший в запросе. + */ +message SendResponse { + StatusCode status = 1; + string error = 2; + uint64 echo_seq_id = 3; +} + +/** + * @message SerialLogEntry + * @brief Описание одного события (TX или RX) на уровне сырых байтов. + * + * Это «протокол‑верификация»: каждая запись сохраняется в журнал + * и может быть передана клиенту в реальном времени. + * + * @param dir Направление трафика (TX – передача, RX – приём). + * @param timestamp Время события в миллисекундах с начала эпохи Unix. + * @param raw_data Точные байты, полученные/отправленные в этом событии. + * @param note Необязательная строка‑комментарий (например, + * «checksum ok», «frame start», …). + */ +message SerialLogEntry { + /** @brief Направление трафика. */ + enum Direction { + /** @brief Пакет был отправлен в порт. */ + TX = 0; + /** @brief Пакет был получен из порта. */ + RX = 1; + } + Direction dir = 1; + uint64 timestamp = 2; + bytes raw_data = 3; + string note = 4; +} + +/** + * @message LogStreamRequest + * @brief Параметры подписки на поток журнала. + * + * @param from_start Если true – клиент получает *все* накопленные записи, + * иначе – только новые, появившиеся после установления + * соединения. + */ +message LogStreamRequest { + bool from_start = 1; +} + +/** + * @message DeviceList + * @brief Обёртка, возвращающая список найденных последовательных портов. + * + * @param devices Список `SerialDevice`. + */ +message DeviceList { + repeated SerialDevice devices = 1; +} + +/* -------------------------------------------------------------------------- */ +/* Service definition – набор RPC‑методов, которые реализует backend */ +/* -------------------------------------------------------------------------- */ + +/** + * @service SerialService + * @brief gRPC‑сервис, предоставляющий весь функционал работы с + * последовательным портом и журналированием. + * + * Каждый метод описан ниже, вместе с его параметрами и возвращаемыми + * типами. + */ +service SerialService { + + // ---------------------------------------------------------------------- + // Device discovery + // ---------------------------------------------------------------------- + /** + * @brief Возвращает список всех последовательных портов, обнаруженных в системе. + * + * @param request Пустое сообщение (`google.protobuf.Empty`). + * @return `DeviceList` – массив `SerialDevice`. + */ + rpc ListDevices (google.protobuf.Empty) returns (DeviceList); + + // ---------------------------------------------------------------------- + // Connection handling + // ---------------------------------------------------------------------- + /** + * @brief Открывает (или переоткрывает) последовательный порт с указанными + * параметрами. + * + * @param cfg Полный набор параметров соединения (`SerialConfig`). + * @return `ConfigResponse` – статус операции. + */ + rpc Open (SerialConfig) returns (ConfigResponse); + + /** + * @brief Закрывает текущий открытый порт (если он был открыт). + * + * @param request Пустое сообщение. + * @return `ConfigResponse` – статус операции. + */ + rpc Close (google.protobuf.Empty) returns (ConfigResponse); + + /** + * @brief Возвращает конфигурацию, с которой в данный момент открыт порт. + * + * Если порт закрыт, возвращается пустой `SerialConfig`. + * + * @param request Пустое сообщение. + * @return `SerialConfig`. + */ + rpc GetCurrentConfig (google.protobuf.Empty) returns (SerialConfig); + + // ---------------------------------------------------------------------- + // Packet transmission + // ---------------------------------------------------------------------- + /** + * @brief Отправка одного пакета в открытый последовательный порт. + * + * Если в `SerialConfig.packet_size` задан фиксированный размер, то + * длина `payload` должна строго соответствовать этому размеру. + * + * @param pkt Пакет для отправки (`Packet`). + * @return `SendResponse` – результат записи в порт. + */ + rpc SendPacket (Packet) returns (SendResponse); + + /** + * @brief Получить один пакет из входящего буфера. + * + * Если буфер пуст, сервер возвращает gRPC‑ошибку `NOT_FOUND`. + * + * @param request Пустое сообщение. + * @return `Packet` (или ошибка). + */ + rpc ReceivePacket (google.protobuf.Empty) returns (Packet); + + // ---------------------------------------------------------------------- + // Logging / protocol verification + // ---------------------------------------------------------------------- + /** + * @brief Прямо вставить произвольную запись в журнал. + * + * Полезно для отладки: можно «симулировать» приходящие данные, + * не взаимодействуя с реальным оборудованием. + * + * @param entry Запись журнала (`SerialLogEntry`). + * @return Пустое сообщение (`google.protobuf.Empty`). + */ + rpc LogRaw (SerialLogEntry) returns (google.protobuf.Empty); + + /** + * @brief Подписка на поток всех событий журнала (TX и RX). + * + * Поток остаётся открытым до тех пор, пока клиент не отменит RPC. + * + * @param req Параметры подписки (`LogStreamRequest`). + * @return Поток `SerialLogEntry`. + */ + rpc StreamLog (LogStreamRequest) returns (stream SerialLogEntry); +} \ No newline at end of file