From b1df84a87fb63b40b7819ac2f7ee390c1d472f11 Mon Sep 17 00:00:00 2001 From: "Timur A. Fatkhullin" Date: Tue, 10 Feb 2026 19:12:16 +0300 Subject: [PATCH] ... --- .qtcreator/CMakeLists.txt.user.25c9513 | 746 ++++++++++++++ CMakeLists.txt | 359 ++++--- mcc_netserver.h | 1245 ++++++++++++++++++++++++ mcc_netserver_endpoint.h | 512 ++++++++++ mcc_netserver_proto.h | 821 ++++++++++++++++ 5 files changed, 3547 insertions(+), 136 deletions(-) create mode 100644 .qtcreator/CMakeLists.txt.user.25c9513 create mode 100644 mcc_netserver.h create mode 100644 mcc_netserver_endpoint.h create mode 100644 mcc_netserver_proto.h diff --git a/.qtcreator/CMakeLists.txt.user.25c9513 b/.qtcreator/CMakeLists.txt.user.25c9513 new file mode 100644 index 0000000..878bdfa --- /dev/null +++ b/.qtcreator/CMakeLists.txt.user.25c9513 @@ -0,0 +1,746 @@ + + + + + + EnvironmentId + {25c95133-c398-49ba-8aa0-e9daf4073ad0} + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + true + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + 2 + UTF-8 + false + 4 + false + 0 + 80 + true + true + 1 + 0 + false + true + false + 2 + true + true + 0 + 8 + true + false + 1 + true + true + true + *.md, *.MD, Makefile + false + true + true + + + + ProjectExplorer.Project.PluginSettings + + + true + false + true + true + true + true + + false + + + 0 + true + + true + true + Builtin.DefaultTidyAndClazy + 4 + true + + + + true + + + true + true + + 0 + + + + ProjectExplorer.Project.Target.0 + + Desktop + true + Desktop + Desktop + {0a48682c-1ff4-4142-9b6d-ae2f5a4dfc82} + 0 + 0 + 1 + + Debug + 2 + false + + -DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON +-DCMAKE_C_COMPILER:STRING=%{Compiler:Executable:C} +-DCMAKE_GENERATOR:STRING=Unix Makefiles +-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake +-DCMAKE_CXX_COMPILER:STRING=%{Compiler:Executable:Cxx} +-DCMAKE_BUILD_TYPE:STRING=Debug + /home/timur/PROGRAMS/C++/mcc/build/Desktop-Debug + + + + + all + + false + + true + Собрать + CMakeProjectManager.MakeStep + + 1 + Сборка + Сборка + ProjectExplorer.BuildSteps.Build + + + + + + clean + + false + + true + Собрать + CMakeProjectManager.MakeStep + + 1 + Очистка + Очистка + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Отладка + CMakeProjectManager.CMakeBuildConfiguration + 0 + 1 + + + 0 + Развёртывание + Развёртывание + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + ProjectExplorer.DefaultDeployConfiguration + + + + + + + + + false + + true + Собрать + ApplicationManagerPlugin.Deploy.CMakePackageStep + + + install-package --acknowledge + true + Install Application Manager package + ApplicationManagerPlugin.Deploy.InstallPackageStep + + + + + + + + 2 + Развёртывание + Развёртывание + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + ApplicationManagerPlugin.Deploy.Configuration + + 2 + + false + + false + + + true + true + 0 + true + /usr/bin/valgrind + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + + CMakeProjectManager.CMakeRunConfiguration. + mcc_telemetry_test + false + + true + true + true + %{RunConfig:Executable:Path} + + + true + true + 0 + true + /usr/bin/valgrind + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + + CMakeProjectManager.CMakeRunConfiguration. + mcc_coord_test + false + + true + true + true + %{RunConfig:Executable:Path} + + + true + true + 0 + true + /usr/bin/valgrind + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + + CMakeProjectManager.CMakeRunConfiguration. + mcc_pzone_test + false + + true + true + true + %{RunConfig:Executable:Path} + + 3 + + 1 + + + 0 + Развёртывание + Развёртывание + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + ProjectExplorer.DefaultDeployConfiguration + + + + + + + + + false + + true + Собрать + ApplicationManagerPlugin.Deploy.CMakePackageStep + + + install-package --acknowledge + true + Install Application Manager package + ApplicationManagerPlugin.Deploy.InstallPackageStep + + + + + + + + 2 + Развёртывание + Развёртывание + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + ApplicationManagerPlugin.Deploy.Configuration + + 2 + + true + true + 0 + true + /usr/bin/valgrind + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + + CMakeProjectManager.CMakeRunConfiguration. + mcc_telemetry_test + false + + true + true + true + %{RunConfig:Executable:Path} + + + true + true + 0 + true + /usr/bin/valgrind + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + + CMakeProjectManager.CMakeRunConfiguration. + mcc_coord_test + false + + true + true + true + %{RunConfig:Executable:Path} + + + true + true + 0 + true + /usr/bin/valgrind + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + + CMakeProjectManager.CMakeRunConfiguration. + mcc_pzone_test + false + + true + true + true + %{RunConfig:Executable:Path} + + 3 + + + + ProjectExplorer.Project.Target.1 + + Desktop + true + Clang + Clang + {609869bc-d303-4485-8b34-3f43a08cec9e} + 0 + 0 + 3 + + Debug + 2 + false + + -DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON +-DCMAKE_C_COMPILER:STRING=%{Compiler:Executable:C} +-DCMAKE_GENERATOR:STRING=Ninja +-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake +-DCMAKE_PREFIX_PATH:STRING=%{Qt:QT_INSTALL_PREFIX} +-DQT_QMAKE_EXECUTABLE:STRING=%{Qt:qmakeExecutable} +-DCMAKE_CXX_COMPILER:STRING=%{Compiler:Executable:Cxx} +-DCMAKE_BUILD_TYPE:STRING=Debug + /home/timur/PROGRAMS/C++/mcc/build/Clang-Debug + + + + + all + + false + + true + Собрать + CMakeProjectManager.MakeStep + + 1 + Сборка + Сборка + ProjectExplorer.BuildSteps.Build + + + + + + clean + + false + + true + Собрать + CMakeProjectManager.MakeStep + + 1 + Очистка + Очистка + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Отладка + CMakeProjectManager.CMakeBuildConfiguration + 0 + 3 + + + 0 + Развёртывание + Развёртывание + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + ProjectExplorer.DefaultDeployConfiguration + + + + + + + + + false + + true + Собрать + ApplicationManagerPlugin.Deploy.CMakePackageStep + + + install-package --acknowledge + true + Install Application Manager package + ApplicationManagerPlugin.Deploy.InstallPackageStep + + + + + + + + 2 + Развёртывание + Развёртывание + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + ApplicationManagerPlugin.Deploy.Configuration + + 2 + + false + + false + + + true + true + 0 + true + /usr/bin/valgrind + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + + CMakeProjectManager.CMakeRunConfiguration. + exe + false + + true + true + true + %{RunConfig:Executable:Path} + + + true + true + 0 + true + /usr/bin/valgrind + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + + CMakeProjectManager.CMakeRunConfiguration. + mcc_telemetry_test + false + + true + true + true + %{RunConfig:Executable:Path} + + + true + true + 0 + true + /usr/bin/valgrind + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + + CMakeProjectManager.CMakeRunConfiguration. + mcc_coord_test + false + + true + true + true + %{RunConfig:Executable:Path} + + + true + true + 0 + true + /usr/bin/valgrind + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + + CMakeProjectManager.CMakeRunConfiguration. + mcc_pzone_test + false + + true + true + true + %{RunConfig:Executable:Path} + + 4 + + 1 + + + 0 + Развёртывание + Развёртывание + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + ProjectExplorer.DefaultDeployConfiguration + + + + + + + + + false + + true + Собрать + ApplicationManagerPlugin.Deploy.CMakePackageStep + + + install-package --acknowledge + true + Install Application Manager package + ApplicationManagerPlugin.Deploy.InstallPackageStep + + + + + + + + 2 + Развёртывание + Развёртывание + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + ApplicationManagerPlugin.Deploy.Configuration + + 2 + + true + true + 0 + true + /usr/bin/valgrind + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + + CMakeProjectManager.CMakeRunConfiguration. + exe + false + + true + true + true + %{RunConfig:Executable:Path} + + + true + true + 0 + true + /usr/bin/valgrind + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + + CMakeProjectManager.CMakeRunConfiguration. + mcc_telemetry_test + false + + true + true + true + %{RunConfig:Executable:Path} + + + true + true + 0 + true + /usr/bin/valgrind + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + + CMakeProjectManager.CMakeRunConfiguration. + mcc_coord_test + false + + true + true + true + %{RunConfig:Executable:Path} + + + true + true + 0 + true + /usr/bin/valgrind + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + + CMakeProjectManager.CMakeRunConfiguration. + mcc_pzone_test + false + + true + true + true + %{RunConfig:Executable:Path} + + 4 + + + + ProjectExplorer.Project.TargetCount + 2 + + + Version + 22 + + diff --git a/CMakeLists.txt b/CMakeLists.txt index d691f5e..cdd9a37 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,11 +1,9 @@ cmake_minimum_required(VERSION 3.14) - # ******* MOUNT CONTROL COMPONENTS ******* project(mcc LANGUAGES C CXX Fortran VERSION 0.1) - # set(CMAKE_BUILD_TYPE Release) set(CMAKE_CXX_STANDARD 23) @@ -13,13 +11,27 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake") - # ******* LIBRARY OPTIONS ******* -option(USE_SPDLOG "Use of SPDLOG library (add implementation of logger class based on this library)" ON) -option(USE_ERFA "Use of ERFA library (add implementation of CCTE based on this library)" ON) +option( + USE_SPDLOG + "Use of SPDLOG library (add implementation of logger class based on this library)" + ON +) + +option( + USE_ERFA + "Use of ERFA library (add implementation of CCTE based on this library)" + ON +) + option(USE_BSPLINE_PCM "Use of FITPACK bivariate splines for PCM" ON) +option( + USE_ASIO + "Use of ASIO-library (add generic implementation of network capabilities)" + ON +) option(BUILD_TESTS "Build tests" ON) @@ -30,20 +42,22 @@ include(ExternalProject) # ******* SPDLOG LIBRARY ******* -if (USE_SPDLOG) +if(USE_SPDLOG) set(SPDLOG_USE_STD_FORMAT ON CACHE INTERNAL "Use of C++20 std::format") set(SPDLOG_FMT_EXTERNAL OFF CACHE INTERNAL "Turn off external fmt library") find_package(spdlog CONFIG) - if (NOT ${spdlog_FOUND}) - FetchContent_Declare(spdlog - GIT_REPOSITORY "https://github.com/gabime/spdlog.git" - GIT_TAG "v1.15.1" - GIT_SHALLOW TRUE - GIT_SUBMODULES "" - GIT_PROGRESS TRUE - CMAKE_ARGS "-DSPDLOG_USE_STD_FORMAT=ON -DSPDLOG_FMT_EXTERNAL=OFF" - OVERRIDE_FIND_PACKAGE + if(NOT ${spdlog_FOUND}) + FetchContent_Declare( + spdlog + GIT_REPOSITORY "https://github.com/gabime/spdlog.git" + GIT_TAG "v1.15.1" + GIT_SHALLOW TRUE + GIT_SUBMODULES "" + GIT_PROGRESS TRUE + CMAKE_ARGS + "-DSPDLOG_USE_STD_FORMAT=ON -DSPDLOG_FMT_EXTERNAL=OFF" + OVERRIDE_FIND_PACKAGE ) find_package(spdlog CONFIG) endif() @@ -51,133 +65,192 @@ endif() # ******* ERFA LIBRARY ******* - find_package(PkgConfig REQUIRED) -pkg_check_modules(ERFALIB IMPORTED_TARGET GLOBAL erfa) - -if (NOT ERFALIB_FOUND) - message(STATUS "\tfetch erfa-lib ...") - # ExternalProject_Add(erfalib - # PREFIX ${CMAKE_BINARY_DIR}/erfa_lib - # GIT_REPOSITORY "https://github.com/liberfa/erfa.git" - # GIT_TAG "v2.0.1" - # UPDATE_COMMAND "" - # PATCH_COMMAND "" - # LOG_CONFIGURE 1 - # CONFIGURE_COMMAND meson setup --reconfigure -Ddefault_library=static -Dbuildtype=release - # -Dprefix=${CMAKE_BINARY_DIR}/erfa_lib -Dlibdir= -Dincludedir= -Ddatadir= - # BUILD_COMMAND ninja -C - # INSTALL_COMMAND meson install -C - # BUILD_BYPRODUCTS ${CMAKE_BINARY_DIR}/erfa_lib/liberfa.a - # ) - - # add_library(PkgConfig::ERFALIB STATIC IMPORTED GLOBAL) - # set_target_properties(PkgConfig::ERFALIB PROPERTIES IMPORTED_LOCATION ${CMAKE_BINARY_DIR}/erfa_lib/liberfa.a) - # set_target_properties(PkgConfig::ERFALIB PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_BINARY_DIR}/erfa_lib) - # add_dependencies(PkgConfig::ERFALIB erfalib) - - # set(CACHE{ERFALIB_INCLUDE_DIRS} TYPE PATH VALUE "${CMAKE_BINARY_DIR}/erfa_lib") - # set(CACHE{ERFALIB_LIBRARY_DIRS} TYPE PATH VALUE "${CMAKE_BINARY_DIR}/erfa_lib") - # set(CACHE{ERFALIB_LIBRARIES} TYPE STRING VALUE "erfa;m") - - find_program(MESON_PROG NAMES meson HINTS ENV PATHS) - - if (NOT MESON_PROG) - message(FATAL "meson executable can not be found!!!") - endif() - - find_program(NINJA_PROG NAMES ninja ninja-build) - - if (NOT NINJA_PROG) - message(FATAL "ninja executable can not be found!!!") - endif() - - - FetchContent_Declare(erfalib_project - GIT_REPOSITORY "https://github.com/liberfa/erfa.git" - GIT_TAG "v2.0.1" - GIT_SHALLOW TRUE - GIT_PROGRESS TRUE - ) - - FetchContent_MakeAvailable(erfalib_project) - # message(STATUS "ERFA: ${erfalib_project_SOURCE_DIR}") - - message(STATUS "\tbuild erfa-lib ...") - execute_process( - COMMAND meson setup --reconfigure -Ddefault_library=static -Dbuildtype=release - -Dprefix=${CMAKE_BINARY_DIR}/erfa_lib -Dlibdir= -Dincludedir= -Ddatadir= ${CMAKE_BINARY_DIR}/erfa_lib ${erfalib_project_SOURCE_DIR} - ) - - execute_process( - WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/erfa_lib - COMMAND ninja -C ${CMAKE_BINARY_DIR}/erfa_lib - ) - - execute_process( - WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/erfa_lib - COMMAND meson install -C ${CMAKE_BINARY_DIR}/erfa_lib - ) - - set(ENV{PKG_CONFIG_PATH} "${CMAKE_BINARY_DIR}/erfa_lib/pkgconfig") +if(USE_ERFA) pkg_check_modules(ERFALIB IMPORTED_TARGET GLOBAL erfa) + + if(NOT ERFALIB_FOUND) + message(STATUS "\tfetch erfa-lib ...") + # ExternalProject_Add(erfalib + # PREFIX ${CMAKE_BINARY_DIR}/erfa_lib + # GIT_REPOSITORY "https://github.com/liberfa/erfa.git" + # GIT_TAG "v2.0.1" + # UPDATE_COMMAND "" + # PATCH_COMMAND "" + # LOG_CONFIGURE 1 + # CONFIGURE_COMMAND meson setup --reconfigure -Ddefault_library=static -Dbuildtype=release + # -Dprefix=${CMAKE_BINARY_DIR}/erfa_lib -Dlibdir= -Dincludedir= -Ddatadir= + # BUILD_COMMAND ninja -C + # INSTALL_COMMAND meson install -C + # BUILD_BYPRODUCTS ${CMAKE_BINARY_DIR}/erfa_lib/liberfa.a + # ) + + # add_library(PkgConfig::ERFALIB STATIC IMPORTED GLOBAL) + # set_target_properties(PkgConfig::ERFALIB PROPERTIES IMPORTED_LOCATION ${CMAKE_BINARY_DIR}/erfa_lib/liberfa.a) + # set_target_properties(PkgConfig::ERFALIB PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_BINARY_DIR}/erfa_lib) + # add_dependencies(PkgConfig::ERFALIB erfalib) + + # set(CACHE{ERFALIB_INCLUDE_DIRS} TYPE PATH VALUE "${CMAKE_BINARY_DIR}/erfa_lib") + # set(CACHE{ERFALIB_LIBRARY_DIRS} TYPE PATH VALUE "${CMAKE_BINARY_DIR}/erfa_lib") + # set(CACHE{ERFALIB_LIBRARIES} TYPE STRING VALUE "erfa;m") + + find_program(MESON_PROG NAMES meson HINTS ENV PATHS) + + if(NOT MESON_PROG) + message(FATAL "meson executable can not be found!!!") + endif() + + find_program(NINJA_PROG NAMES ninja ninja-build) + + if(NOT NINJA_PROG) + message(FATAL "ninja executable can not be found!!!") + endif() + + FetchContent_Declare( + erfalib_project + GIT_REPOSITORY "https://github.com/liberfa/erfa.git" + GIT_TAG "v2.0.1" + GIT_SHALLOW TRUE + GIT_PROGRESS TRUE + ) + + FetchContent_MakeAvailable(erfalib_project) + # message(STATUS "ERFA: ${erfalib_project_SOURCE_DIR}") + + message(STATUS "\tbuild erfa-lib ...") + execute_process( + COMMAND + meson setup --reconfigure -Ddefault_library=static + -Dbuildtype=release -Dprefix=${CMAKE_BINARY_DIR}/erfa_lib + -Dlibdir= -Dincludedir= -Ddatadir= ${CMAKE_BINARY_DIR}/erfa_lib + ${erfalib_project_SOURCE_DIR} + ) + + execute_process( + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/erfa_lib + COMMAND ninja -C ${CMAKE_BINARY_DIR}/erfa_lib + ) + + execute_process( + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/erfa_lib + COMMAND meson install -C ${CMAKE_BINARY_DIR}/erfa_lib + ) + + set(ENV{PKG_CONFIG_PATH} "${CMAKE_BINARY_DIR}/erfa_lib/pkgconfig") + pkg_check_modules(ERFALIB IMPORTED_TARGET GLOBAL erfa) + endif() + + message(STATUS "ERFA LIBS: ${ERFALIB_LIBRARIES}") + message(STATUS "ERFA LIB PATHS: ${ERFALIB_LIBRARY_DIRS}") + message(STATUS "ERFA INC PATHS: ${ERFALIB_INCLUDE_DIRS}") endif() +if(USE_ASIO) + pkg_check_modules(ASIOLIB IMPORTED_TARGET GLOBAL asio) -message(STATUS "ERFA LIBS: ${ERFALIB_LIBRARIES}") -message(STATUS "ERFA LIB PATHS: ${ERFALIB_LIBRARY_DIRS}") -message(STATUS "ERFA INC PATHS: ${ERFALIB_INCLUDE_DIRS}") + if(NOT ASIOLIB_FOUND) + message(STATUS "\tfetch asio-lib ...") + FetchContent_Declare( + asiolib_project + PREFIX + ${CMAKE_BINARY_DIR}/asio + GIT_REPOSITORY "https://github.com/chriskohlhoff/asio.git" + GIT_TAG "asio-1-36-0" + GIT_SHALLOW 1 + ) -if (USE_BSPLINE_PCM) + FetchContent_MakeAvailable(asiolib_project) + endif() + + execute_process( + WORKING_DIRECTORY ${asiolib_project_SOURCE_DIR}/asio + COMMAND ./autogen.sh + ) + + execute_process( + WORKING_DIRECTORY ${asiolib_project_SOURCE_DIR}/asio + COMMAND ./configure --prefix=${asiolib_project_SOURCE_DIR} + ) + + set(ENV{PKG_CONFIG_PATH} "${asiolib_project_SOURCE_DIR}/asio") + pkg_check_modules(ASIOLIB IMPORTED_TARGET GLOBAL asio) + + message(STATUS "ASIO INC PATHS: ${ASIOLIB_INCLUDE_DIRS}") +endif() + +if(USE_BSPLINE_PCM) # fitpack by P. Dierckx add_subdirectory(fitpack) endif() -set(MCC_SRC mcc_concepts.h mcc_constants.h mcc_epoch.h mcc_angle.h mcc_coordinate.h mcc_error.h - mcc_traits.h mcc_utils.h mcc_ccte_iers.h mcc_ccte_iers_default.h mcc_ccte_erfa.h mcc_pzone.h - mcc_pzone_container.h mcc_pcm.h mcc_telemetry.h mcc_serializer.h) +set(MCC_SRC + mcc_concepts.h + mcc_constants.h + mcc_epoch.h + mcc_angle.h + mcc_coordinate.h + mcc_error.h + mcc_traits.h + mcc_utils.h + mcc_pzone.h + mcc_pzone_container.h + mcc_pcm.h + mcc_telemetry.h + mcc_serialization_common.h + mcc_deserializer.h + mcc_serializer.h +) -if (USE_SPDLOG) +if(USE_SPDLOG) list(APPEND MCC_SRC mcc_spdlog.h) endif() +if(USE_ERFA) + list(APPEND MCC_SRC mcc_ccte_iers.h mcc_ccte_iers_default.h mcc_ccte_erfa.h) +endif() + +if(USE_ASIO) + list( + APPEND MCC_SRC + mcc_netserver_endpoint.h + mcc_netserver_proto.h + mcc_netserver.h + ) +endif() + add_library(${PROJECT_NAME} INTERFACE ${MCC_SRC}) target_compile_features(${PROJECT_NAME} INTERFACE cxx_std_23) -target_link_libraries(${PROJECT_NAME} INTERFACE PkgConfig::ERFALIB) -# target_link_libraries(${PROJECT_NAME} INTERFACE PkgConfig::ERFALIB fitpack) -target_include_directories( - ${PROJECT_NAME} - INTERFACE - # $ - $ - $ -) -if (USE_BSPLINE_PCM) - target_compile_definitions(${PROJECT_NAME} INTERFACE USE_BSPLINE_PCM) - target_link_libraries(${PROJECT_NAME} INTERFACE fitpack) - # target_include_directories( - # ${PROJECT_NAME} - # INTERFACE - # $) + +if(USE_ERFA) + target_link_libraries(${PROJECT_NAME} INTERFACE PkgConfig::ERFALIB) + target_include_directories( + ${PROJECT_NAME} + INTERFACE + # $ + $ + $ + ) +else() + target_include_directories( + ${PROJECT_NAME} + INTERFACE + $ + $ + ) endif() -# get_target_property(ZZ ${PROJECT_NAME} INTERFACE_INCLUDE_DIRECTORIES) +if(USE_BSPLINE_PCM) + target_compile_definitions(${PROJECT_NAME} INTERFACE USE_BSPLINE_PCM) + target_link_libraries(${PROJECT_NAME} INTERFACE fitpack) +endif() -# message(STATUS "INT: ${ZZ}") +if(USE_ASIO) + target_link_libraries(${PROJECT_NAME} INTERFACE PkgConfig::ASIOLIB) +endif() - - -# add_executable(exe EXCLUDE_FROM_ALL main.cpp) -# target_link_libraries(exe PUBLIC ${PROJECT_NAME}) - -# get_target_property(ZZ exe INCLUDE_DIRECTORIES) - -# message(STATUS "INT: ${ZZ_STRING}") - - -if (BUILD_TESTS) +if(BUILD_TESTS) add_executable(mcc_telemetry_test tests/mcc_telemetry_test.cpp) target_link_libraries(mcc_telemetry_test PRIVATE ${PROJECT_NAME}) @@ -192,34 +265,48 @@ else() target_link_libraries(just_stub PUBLIC ${PROJECT_NAME}) endif() - include(CMakePackageConfigHelpers) -write_basic_package_version_file("${PROJECT_NAME}ConfigVersion.cmake" - VERSION ${PROJECT_VERSION} - COMPATIBILITY SameMajorVersion) +write_basic_package_version_file( + "${PROJECT_NAME}ConfigVersion.cmake" + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion +) +set(MCC_CONFIG_INSTALLDIR + ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME} + CACHE PATH + "install path for generated library config files" +) +set(MCC_HEADERS_INSTALLDIR + ${CMAKE_INSTALL_INCLUDEDIR} + CACHE PATH + "install path for headers" +) -set(MCC_CONFIG_INSTALLDIR ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME} CACHE PATH "install path for generated library config files") -set(MCC_HEADERS_INSTALLDIR ${CMAKE_INSTALL_INCLUDEDIR} CACHE PATH "install path for headers") - -configure_package_config_file("${PROJECT_SOURCE_DIR}/${PROJECT_NAME}Config.cmake.in" - "${PROJECT_BINARY_DIR}/${PROJECT_NAME}Config.cmake" - INSTALL_DESTINATION - ${MCC_CONFIG_INSTALLDIR} - PATH_VARS MCC_HEADERS_INSTALLDIR) +configure_package_config_file( + "${PROJECT_SOURCE_DIR}/${PROJECT_NAME}Config.cmake.in" + "${PROJECT_BINARY_DIR}/${PROJECT_NAME}Config.cmake" + INSTALL_DESTINATION ${MCC_CONFIG_INSTALLDIR} + PATH_VARS MCC_HEADERS_INSTALLDIR +) #install(EXPORT ${PROJECT_NAME}_Targets FILE ${PROJECT_NAME}Targets.cmake NAMESPACE ${PROJECT_NAME_NAMESPACE}:: DESTINATION ${MCC_CONFIG_INSTALLDIR}) #install(FILES "${PROJECT_BINARY_DIR}/${PROJECT_NAME}Config.cmake" "${PROJECT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake" DESTINATION ${MCC_CONFIG_INSTALLDIR}) #install(FILES ${MCC_SRC} DESTINATION include/${PROJECT_NAME}) - # uninstall target if(NOT TARGET uninstall) configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/cmake_uninstall.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake" - IMMEDIATE @ONLY) + IMMEDIATE + @ONLY + ) - add_custom_target(uninstall - COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake) + add_custom_target( + uninstall + COMMAND + ${CMAKE_COMMAND} -P + ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake + ) endif() diff --git a/mcc_netserver.h b/mcc_netserver.h new file mode 100644 index 0000000..daffd78 --- /dev/null +++ b/mcc_netserver.h @@ -0,0 +1,1245 @@ +#pragma once + +/* MOUNT CONTROL COMPONENTS LIBRARY */ + + +/* A VERY SIMPLE NETWORK SERVER IMPLEMENTATION */ + + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#if __has_include() // POSIX +#define FORK_EXISTS 1 +#include +#include +#endif + + + +#include "mcc_generics.h" +#include "mcc_netserver_endpoint.h" +#include "mcc_netserver_proto.h" +#include "mcc_traits.h" + + +namespace mcc::network +{ + +enum class MccGenericMountNetworkServerErrorCode : int { + ERROR_OK, + ERROR_MOUNT_INIT, + ERROR_MOUNT_STOP, + ERROR_MOUNT_SET_TARGET, + ERROR_MOUNT_GET_TELEMETRY, + ERROR_MOUNT_SLEW, + ERROR_MOUNT_MOVE, + ERROR_MOUNT_TRACK +}; + +struct MccGenericMountNetworkServerErrorCategory : std::error_category { + const char* name() const noexcept + { + return "MCC-GENERIC-MOUNT-NETWORK-SERVER"; + } + + std::string message(int ec) const + { + MccGenericMountNetworkServerErrorCode err = static_cast(ec); + + switch (err) { + case MccGenericMountNetworkServerErrorCode::ERROR_OK: + return "OK"; + case MccGenericMountNetworkServerErrorCode::ERROR_MOUNT_INIT: + return "mount init error"; + case MccGenericMountNetworkServerErrorCode::ERROR_MOUNT_STOP: + return "mount stop error"; + case MccGenericMountNetworkServerErrorCode::ERROR_MOUNT_SET_TARGET: + return "mount set target error"; + case MccGenericMountNetworkServerErrorCode::ERROR_MOUNT_GET_TELEMETRY: + return "mount get telemetry error"; + case MccGenericMountNetworkServerErrorCode::ERROR_MOUNT_SLEW: + return "mount slewing error"; + case MccGenericMountNetworkServerErrorCode::ERROR_MOUNT_MOVE: + return "mount moving error"; + case MccGenericMountNetworkServerErrorCode::ERROR_MOUNT_TRACK: + return "mount tracking error"; + defaut: + return "unknown"; + }; + } + + static const MccGenericMountNetworkServerErrorCategory& get() + { + static const MccGenericMountNetworkServerErrorCategory constInst; + return constInst; + } +}; + +inline std::error_code make_error_code(MccGenericMountNetworkServerErrorCode ec) +{ + return std::error_code(static_cast(ec), MccGenericMountNetworkServerErrorCategory::get()); +} + +} // namespace mcc::network + + +namespace std +{ +template <> +class is_error_code_enum : public true_type +{ +}; +} // namespace std + + +namespace mcc::network +{ + + +namespace traits +{ + +template +concept mcc_endpoint_c = std::derived_from || std::derived_from || + std::derived_from || + std::derived_from; + +template +static constexpr bool is_serial_proto = std::derived_from; + +template +static constexpr bool is_tcp_proto = + std::derived_from || std::derived_from; + +template +static constexpr bool is_local_stream_proto = std::derived_from || + std::derived_from; + +template +static constexpr bool is_local_seqpack_proto = std::derived_from || + std::derived_from; + + +} // namespace traits + + + +/* A GENERIC NETWORK SERVER IMPLEMENTATION */ + +template +class MccGenericNetworkServer : public LoggerT +{ +public: + using LoggerT::logDebug; + using LoggerT::logError; + using LoggerT::logInfo; + using LoggerT::logTrace; + using LoggerT::logWarn; + + static constexpr std::chrono::duration DEFAULT_RCV_TIMEOUT = std::chrono::hours(12); + static constexpr std::chrono::duration DEFAULT_SND_TIMEOUT = std::chrono::milliseconds(2000); + + typedef std::vector handle_message_func_result_t; + // handle received message user function + typedef std::function handle_message_func_t; + + + MccGenericNetworkServer(asio::io_context& ctx, const handle_message_func_t& func) + requires std::is_default_constructible_v + : _asioContext(ctx), _handleMessageFunc(func), _stopSignal(ctx), _restartSignal(ctx) + { + std::stringstream st; + st << std::this_thread::get_id(); + + logInfo(std::format("Create generic network server instance (thread ID = {})", st.str())); + } + + template + MccGenericNetworkServer(asio::io_context& ctx, const handle_message_func_t& func, LoggerCtorArgsTs&&... log_args) + requires(not std::is_default_constructible_v) + : LoggerT(std::forward(log_args)...), + _asioContext(ctx), + _handleMessageFunc(func), + _stopSignal(ctx), + _restartSignal(ctx), + _sessionThreadPool(7) + { + std::stringstream st; + st << std::this_thread::get_id(); + + logInfo(std::format("Create MccGenericNetworkServer class instance (thread ID = {})", st.str())); + } + + // MccNetworkServer(asio::io_context& ctx, const handle_message_func_t& func, LoggerT logger = MccNullLogger{}) + // : _asioContext(ctx), _handleMessageFunc(func), _stopSignal(ctx), _restartSignal(ctx) + // { + // std::stringstream st; + // st << std::this_thread::get_id(); + + // logInfo(std::format("Create mount server instance (thread ID = {})", st.str())); + // } + + virtual ~MccGenericNetworkServer() + { + std::stringstream st; + st << std::this_thread::get_id(); + + logInfo(std::format("Delete generic network server instance (thread ID = {}) ...", st.str())); + + stopListening(); + + _sessionThreadPool.stop(); + _sessionThreadPool.join(); + + disconnectClients(); + } + + + template + asio::awaitable listen(std::derived_from auto endpoint, CtorArgTs&&... ctor_args) + { + if (!endpoint.isValid()) { + logError(std::format("Cannot start listening! Invalid endpoint string representation ('{}')!", + endpoint.endpoint())); + co_return; + } + + // add root path to endpoint one + std::filesystem::path pt("/"); + + + if (endpoint.isLocalSerial()) { + pt += endpoint.path(); + + asio::serial_port s_port(_asioContext); + + std::error_code ec; + + if constexpr (sizeof...(CtorArgTs)) { // options + setSerialOpts(s_port, std::forward(ctor_args)...); + } + + s_port.open(pt.string(), ec); + if (ec) { + logError(std::format("Cannot open serial device '{}' (Error = '{}')!", pt.string(), ec.message())); + co_return; + } + + // asio::co_spawn(_asioContext, listen(std::move(s_port)), asio::detached); + co_await listen(std::move(s_port)); + } else if (endpoint.isLocal()) { + // create abstract namespace socket endpoint if its path starts from '@' symbol + endpoint.makeAbstract('@'); + + // if (endpoint.path()[0] == '\0') { // abstract namespace + // std::string p; + // std::ranges::copy(endpoint.path(), std::back_inserter(p)); + // p.insert(p.begin() + 1, '/'); // insert after '\0' symbol + // pt = p; + // } else { + // pt += endpoint.path(); + // } + + if (endpoint.isLocalStream()) { + co_await listen(asio::local::stream_protocol::endpoint(endpoint.path(pt.string()))); + } else if (endpoint.isLocalSeqpacket()) { + co_await listen(asio::local::seq_packet_protocol::endpoint(endpoint.path(pt.string()))); + } else { + co_return; // it must not be!!!! + } + } else if (endpoint.isTCP()) { + // resolve hostname + try { + asio::ip::tcp::resolver res(_asioContext); + + auto r_result = co_await res.async_resolve(endpoint.host(), endpoint.portView(), asio::use_awaitable); + + logInfo(std::format("Resolve hostname <{}> to {} IP-addresses", endpoint.host(), r_result.size())); + + + bool exit_flag = false; + asio::ip::tcp::acceptor acc(_asioContext); + + for (auto const& epn : r_result) { + try { + // std::stringstream st; + + // logDebug("Create connection acceptor for endpoint <{}> ...", + // epn.address().to_string()); + acc = asio::ip::tcp::acceptor(_asioContext, epn); + // st << acc.local_endpoint(); + exit_flag = true; + break; + } catch (const std::system_error& err) { + logError( + std::format("An error occuring while creating connection acceptor (ec = {})", err.what())); + continue; + } + } + if (!exit_flag) { + logError("Cannot start listening on any resolved endpoints!"); + co_return; + } + + _tcpAcceptors.emplace_back(&acc); + + logInfo( + std::format("Start listening at <{}> endpoint ...", acc.local_endpoint().address().to_string())); + + // start accepting connections + for (;;) { + auto sock = co_await acc.async_accept(asio::use_awaitable); + // start new client session + asio::co_spawn(_asioContext, startSession(std::move(sock)), asio::detached); + } + + } catch (const std::system_error& err) { + logError( + std::format("An error occured while trying to start accepting connections! ec = '{}'", err.what())); + } + } + } + + template + asio::awaitable listen(EpnT endpoint) + { + using epn_t = std::decay_t; + + std::error_code ec; + + if constexpr (traits::is_serial_proto) { + // first, check if port is open + if (!endpoint.is_open()) { + if (ec) { + // ?????????? + logError("Serial port was not open! Do not start waiting for commands!"); + } + } else { + asio::co_spawn(_asioContext, startSession(std::move(endpoint)), asio::detached); + } + + } else if constexpr (traits::is_tcp_proto || traits::is_local_stream_proto || + traits::is_local_seqpack_proto) { + try { + std::stringstream st; + + st << endpoint; + logDebug(std::format("Create connection acceptor for endpoint <{}> ...", st.str())); + auto acc = typename epn_t::protocol_type::acceptor(_asioContext, endpoint); + + st.str(""); + st << acc.local_endpoint(); + logInfo(std::format("Start listening at <{}> endpoint ...", st.str())); + + + if constexpr (traits::is_tcp_proto) { + _tcpAcceptors.emplace_back(&acc); + } else if constexpr (traits::is_local_stream_proto) { + _localStreamAcceptors.emplace_back(&acc); + } else if constexpr (traits::is_local_seqpack_proto) { + _localSeqpackAcceptors.emplace_back(&acc); + } else { + static_assert(false, "INVALID ENDPOINT!!!"); + } + + // start accepting connections + for (;;) { + st.str(""); + st << std::this_thread::get_id(); + logDebug(std::format("Start accepting new connections (thread ID = {}) ...", st.str())); + + auto sock = co_await acc.async_accept(asio::use_awaitable); + + // start new client session + logDebug("Spawn new user session ..."); + + // asio::co_spawn(_asioContext, startSession(std::move(sock)), asio::detached); + + // use of sessions own thread pool + asio::co_spawn(_sessionThreadPool, startSession(std::move(sock)), asio::detached); + + logDebug("The session was spawned"); + } + + + } catch (const std::system_error& err) { + logError( + std::format("An error occured while trying to start accepting connections! ec = '{}'", err.what())); + } + } else { + static_assert(false, "INVALID ENDPOINT!!!"); + } + + co_return; + } + + + // close listening on all endpoints + void stopListening() + { + std::error_code ec; + + logInfo("Close all listening endpoints ..."); + + auto num = + _serialPorts.size() + _tcpAcceptors.size() + _localStreamAcceptors.size() + _localSeqpackAcceptors.size(); + if (!num) { + logInfo("There are no listening ports/sockets!"); + return; + } + + auto close_func = [this](auto& acc_ptrs, std::string_view desc) { + size_t N = 0, M = 0; + std::error_code ec; + + if (acc_ptrs.size()) { + logInfo(std::format("Close {} acceptors ...", desc)); + + for (auto& acc : acc_ptrs) { + acc->close(ec); + if (ec) { + logError(std::format("Cannot close {} acceptor! ec = '{}'", desc, ec.message())); + } else { + ++M; + } + ++N; + } + + logDebug(std::format("{} from {} {} acceptors were closed!", M, N, desc)); + + // pointers are invalidated here, so clear its container + acc_ptrs.clear(); + } + }; + + close_func(_tcpAcceptors, "TCP socket"); + close_func(_localStreamAcceptors, "local stream socket"); + close_func(_localSeqpackAcceptors, "local seqpack socket"); + + + logInfo("The all server listening endpoints were closed!"); + } + + void disconnectClients() + { + auto disconn_func = [this](std::ranges::input_range auto& ptrs) { + std::error_code ec; + for (auto& ptr : ptrs) { + // ptr->cancel(ec); + // if (ec) { + // logWarn("socket_base::cancel: an error occured (ec = {})", ec.message()); + // } + + ptr->shutdown(asio::socket_base::shutdown_both, ec); + if (ec) { + logWarn(std::format("socket_base::shutdown: an error occured (ec = {})", ec.message())); + } + + ptr->close(ec); + if (ec) { + logWarn(std::format("socket_base::close: an error occured (ec = {})", ec.message())); + } + } + }; + + logInfo("Close all client connections ..."); + + if (_serialPorts.empty() && _localStreamSockets.empty() && _localSeqpackSockets.empty() && + _tcpSockets.empty()) { + logInfo("There were no active client connections! Skip!"); + } + + if (_serialPorts.size()) { + std::lock_guard lock_g(_serialPortsMutex); + + std::error_code ec; + logInfo(std::format("Close serial port clients ({} in total) ...", _serialPorts.size())); + for (auto& ptr : _serialPorts) { + ptr->cancel(ec); + if (ec) { + logWarn(std::format("serial_port::cancel: an error occured (ec = {})", ec.message())); + } + ptr->close(ec); + if (ec) { + logWarn(std::format("serial_port::close: an error occured (ec = {})", ec.message())); + } + } + } + + if (_localStreamSockets.size()) { + std::lock_guard lock_g(_localStreamSocketsMutex); + + logInfo( + std::format("Close local stream socket-type clients ({} in total) ...", _localStreamSockets.size())); + disconn_func(_localStreamSockets); + } + + if (_localSeqpackSockets.size()) { + std::lock_guard lock_g(_localSeqpackSocketsMutex); + + logInfo( + std::format("Close local seqpack socket-type clients ({} in total) ...", _localSeqpackSockets.size())); + disconn_func(_localSeqpackSockets); + } + + if (_tcpSockets.size()) { + std::lock_guard lock_g(_tcpSocketsMutex); + + logInfo(std::format("Close TCP socket-type clients ({} in total) ...", _tcpSockets.size())); + disconn_func(_tcpSockets); + } + + logInfo("Client connection were closed!"); + } + + void daemonize() + { +#ifdef FORK_EXISTS + logInfo("Daemonize the server ..."); + + _asioContext.notify_fork(asio::execution_context::fork_prepare); + + auto tmp_path = std::filesystem::temp_directory_path(); + if (tmp_path.empty()) { + tmp_path = std::filesystem::current_path().root_path(); + } + + + if (pid_t pid = fork()) { + if (pid > 0) { + exit(0); + } else { + // throw std::system_error(errno, std::generic_category(), "CANNOT FORK 1-STAGE"); + logError("CANNOT FORK 1-STAGE! The server was not daemonized!"); + return; + } + } + + if (setsid() == -1) { + // throw std::system_error(errno, std::generic_category(), "CANNOT FORK SETSID"); + logError("CANNOT FORK SETSID! The server was not daemonized!"); + return; + } + + logInfo(std::format("Try to set the daemon current path to '{}' ...", tmp_path.string())); + + std::error_code ec{}; + + std::filesystem::current_path(tmp_path, ec); + if (!ec) { + logWarn(std::format("Cannot change current path to '{}'! Ignore!", tmp_path.string())); + } + + umask(0); + + if (pid_t pid = fork()) { + if (pid > 0) { + exit(0); + } else { + // throw std::system_error(errno, std::generic_category(), "CANNOT FORK 2-STAGE"); + logError("CANNOT FORK 2-STAGE! The server was not daemonized!"); + return; + } + } + + // stdin, stdout, stderr + close(0); + close(1); + close(2); + + _asioContext.notify_fork(asio::io_context::fork_child); + + logInfo("The server was daemonized successfully!"); + +#else + logWarn("Host platform is not POSIX one, so cannot daemonize the server!"); +#endif + } + + template , std::ranges::range RRT = std::vector> + void setupSignals(const RST& stop_sig_num = {SIGINT, SIGTERM}, const RRT& restart_sig_num = {SIGUSR1}) + requires(std::convertible_to, int> && + std::convertible_to, int>) + { + for (const int sig : stop_sig_num) { + _stopSignal.add(sig); + } + + _stopSignal.async_wait([this](std::error_code, int signo) { + logInfo(std::format("Stop signal was received (signo = {})", signo)); + + // _handleMessageFunc(MCC_COMMPROTO_KEYWORD_STOP_STR); + // std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // stopListening(); + + // _sessionThreadPool.stop(); + // _sessionThreadPool.join(); + + // disconnectClients(); + + _asioContext.stop(); + }); + + for (const int sig : restart_sig_num) { + _restartSignal.add(sig); + } + + _restartSignal.async_wait([this](std::error_code, int signo) { + logInfo(std::format("Restart signal was received (signo = {})", signo)); + restart(); + }); + } + + void restart() + { + disconnectClients(); + + _restartSignal.async_wait([this](std::error_code, int signo) { + logInfo(std::format("Restart signal was received (signo = {})", signo)); + restart(); + }); + } + +protected: + asio::io_context& _asioContext; + + handle_message_func_t _handleMessageFunc; + + asio::signal_set _stopSignal, _restartSignal; + + std::set _serialPorts; + // std::vector _serialPorts; + + std::vector _tcpAcceptors; + std::vector _localStreamAcceptors; + std::vector _localSeqpackAcceptors; + + std::set _tcpSockets; + std::set _localStreamSockets; + std::set _localSeqpackSockets; + // std::vector _tcpSockets; + // std::vector _localStreamSockets; + // std::vector _localSeqpackSockets; + + std::mutex _serialPortsMutex, _tcpSocketsMutex, _localStreamSocketsMutex, _localSeqpackSocketsMutex; + + asio::thread_pool _sessionThreadPool; + + // helpers + template + void setSerialOpts(asio::serial_port& s_port, OptT&& opt, OptTs&&... opts) + { + std::error_code ec; + + s_port.set_option(opt, ec); + if (ec) { + std::string_view opt_name; + + if constexpr (std::same_as) { + opt_name = "baud rate"; + } else if constexpr (std::same_as) { + opt_name = "parity"; + } else if constexpr (std::same_as) { + opt_name = "flow control"; + } else if constexpr (std::same_as) { + opt_name = "stop bits"; + } else if constexpr (std::same_as) { + opt_name = "char size"; + } + + logError(std::format("Cannot set serial port '{}' option! Just skip!", opt_name)); + } + + if constexpr (sizeof...(OptTs)) { + setSerialOpts(s_port, std::forward(opts)...); + } + } + + + // std::vector handleClientCommand(std::string_view command) + // { + // std::vector resp{MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR.begin(), + // MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR.end()}; + + // return resp; + // } + + + template + asio::awaitable startSession(auto socket, + const RCVT& rcv_timeout = DEFAULT_RCV_TIMEOUT, + const SNDT& snd_timeout = DEFAULT_SND_TIMEOUT) + { + using namespace asio::experimental::awaitable_operators; + + using sock_t = std::decay_t; + + + auto look_for_whole_msg = [](auto const& bytes) { + auto found = std::ranges::search(bytes, MCC_COMMPROTO_STOP_SEQ); + return found.empty() ? std::span(bytes.begin(), bytes.begin()) : std::span(bytes.begin(), found.end()); + }; + + + asio::streambuf sbuff; + size_t nbytes; + std::stringstream st; + std::string r_epn; + + st << std::this_thread::get_id(); + std::string thr_id = st.str(); + st.str(""); + + if constexpr (traits::is_serial_proto) { + st << "serial port: " << socket.native_handle(); + } else { // network sockets + st << socket.remote_endpoint(); + } + + r_epn = st.str(); + if (r_epn.empty()) { // UNIX domain sockets + r_epn = "local"; + } + + logInfo(std::format("Start client session: remote endpoint <{}> (session thread ID = {})", r_epn, thr_id)); + + try { + if constexpr (!traits::is_serial_proto) { + logTrace("Set socket option KEEP_ALIVE to TRUE"); + socket.set_option(asio::socket_base::keep_alive(true)); + } + + if constexpr (traits::is_serial_proto) { + std::lock_guard lock_g(_serialPortsMutex); + _serialPorts.insert(&socket); + } else if constexpr (traits::is_tcp_proto) { + std::lock_guard lock_g(_tcpSocketsMutex); + // _tcpSockets.emplace_back(&socket); + _tcpSockets.insert(&socket); + } else if constexpr (traits::is_local_stream_proto) { + std::lock_guard lock_g(_localStreamSocketsMutex); + // _localStreamSockets.emplace_back(&socket); + _localStreamSockets.insert(&socket); + } else if constexpr (traits::is_local_seqpack_proto) { + std::lock_guard lock_g(_localSeqpackSocketsMutex); + // _localSeqpackSockets.emplace_back(&socket); + _localSeqpackSockets.insert(&socket); + } else { + static_assert(false, "INVALID SOCKET TTYPE!!!"); + } + + + // send buffer sequence + // initiate the second element by "stop-sequence" symbols + std::vector snd_buff_seq{ + {}, {MCC_COMMPROTO_STOP_SEQ.data(), MCC_COMMPROTO_STOP_SEQ.size()}}; + + asio::steady_timer timeout_timer(_asioContext); + std::variant op_res; + + std::error_code ec; + + bool do_read = true; + + // main "client request -- server respond" cycle + for (;;) { + // receive message + + if (do_read) { + logTrace(std::format("Start socket/port reading operation with timeout {} ...", rcv_timeout)); + + if constexpr (traits::is_serial_proto) { + nbytes = 1024; + } else { + nbytes = socket.available(); + } + + auto buff = sbuff.prepare(nbytes ? nbytes : 1); + + // timeout_timer.expires_after(std::chrono::seconds(5)); + timeout_timer.expires_after(rcv_timeout); + + if constexpr (traits::is_local_seqpack_proto) { + asio::socket_base::message_flags oflags; + + op_res = co_await ( + socket.async_receive(buff, oflags, asio::redirect_error(asio::use_awaitable, ec)) || + timeout_timer.async_wait(asio::use_awaitable)); + + } else { + op_res = co_await (asio::async_read(socket, buff, asio::transfer_at_least(1), + asio::redirect_error(asio::use_awaitable, ec)) || + timeout_timer.async_wait(asio::use_awaitable)); + } + + if (ec) { + throw std::system_error(ec); + } + + if (op_res.index()) { + throw std::system_error(std::make_error_code(std::errc::timed_out)); + } else { + nbytes = std::get<0>(op_res); + + logTrace(std::format("{} bytes were received", nbytes)); + + if constexpr (traits::is_local_seqpack_proto) { + if (!nbytes) { // EOF! + throw std::system_error(std::error_code(asio::error::misc_errors::eof)); + } + } + } + + sbuff.commit(nbytes); + } // here, the input stream buffer still contains remaining bytes. try to handle its + + auto start_ptr = static_cast(sbuff.data().data()); + + auto msg = look_for_whole_msg(std::span(start_ptr, sbuff.size())); + if (msg.empty()) { // still not whole message + logTrace(std::format( + "It seems a partial command message was received, so waiting for remaining part ...")); + do_read = true; + continue; + } + + + // extract command without stop sequence symbols + // std::string comm; + // std::ranges::copy(msg | std::views::take(msg.size() - MCC_COMMPROTO_STOP_SEQ.size()), + // std::back_inserter(comm)); + + std::string_view comm{msg.begin(), msg.end() - MCC_COMMPROTO_STOP_SEQ.size()}; + + logDebug(std::format("A command [{}] was received from client (remote endpoint <{}>, thread ID = {})", + comm, r_epn, thr_id)); + + // auto resp = handleClientCommand(comm); + auto resp = _handleMessageFunc(comm); + + // remove received message from the input stream buffer. NOTE: 'msg' is now invalidated!!! + sbuff.consume(msg.size()); + do_read = sbuff.size() == 0; + + + logDebug(std::format("Send respond [{}] to client (remote endpoint <{}>, thread ID = {})", + std::string_view(resp.begin(), resp.end()), r_epn, thr_id)); + + // send server respond to client + snd_buff_seq[0] = {resp.data(), resp.size()}; + + timeout_timer.expires_after(snd_timeout); + if constexpr (traits::is_local_seqpack_proto) { + op_res = + co_await (socket.async_send(snd_buff_seq, 0, asio::redirect_error(asio::use_awaitable, ec)) || + timeout_timer.async_wait(asio::use_awaitable)); + } else { + // nbytes = co_await asio::async_write(socket, snd_buff_seq, asio::use_awaitable); + op_res = co_await ( + asio::async_write(socket, snd_buff_seq, asio::redirect_error(asio::use_awaitable, ec)) || + timeout_timer.async_wait(asio::use_awaitable)); + } + + if (ec) { + throw std::system_error(ec); + } + + if (op_res.index()) { + throw std::system_error(std::make_error_code(std::errc::timed_out)); + } else { + nbytes = std::get<0>(op_res); + logTrace(std::format("{} bytes were sent", nbytes)); + } + + if (nbytes != (resp.size() + MCC_COMMPROTO_STOP_SEQ.size())) { // !!!!!!!!!! + } + } + + } catch (const std::system_error& ex) { + if (ex.code() == std::error_code(asio::error::misc_errors::eof)) { + logInfo(std::format( + "It seems client or server closed the connection (remote endpoint <{}>, thread ID = {})", r_epn, + thr_id)); + } else { + logError(std::format("An error '{}' occured in client session (remote endpoint <{}>, thread ID = {})", + ex.what(), r_epn, thr_id)); + } + } catch (const std::exception& ex) { + logError( + std::format("An unhandled error '{}' occured in client sesssion (remote endpoint <{}>, thread ID = {})", + ex.what(), r_epn, thr_id)); + } catch (...) { + logError(std::format("An unhandled error occured in client sesssion (remote endpoint <{}>, thread ID = {})", + r_epn, thr_id)); + } + + // remove pointer as it is invalidated here (at the exit of the method) + if constexpr (traits::is_serial_proto) { + _serialPorts.erase(&socket); + } else if constexpr (traits::is_tcp_proto) { + _tcpSockets.erase(&socket); + } else if constexpr (traits::is_local_stream_proto) { + _localStreamSockets.erase(&socket); + } else if constexpr (traits::is_local_seqpack_proto) { + _localSeqpackSockets.erase(&socket); + } else { + static_assert(false, "INVALID SOCKET TTYPE!!!"); + } + + logInfo(std::format("Close client session: remote endpoint <{}> (thread ID = {})", r_epn, thr_id)); + + co_return; + } +}; + + +template +class MccGenericMountNetworkServer : public MccGenericNetworkServer +{ + using base_t = MccGenericNetworkServer; + +public: + using typename base_t::handle_message_func_result_t; + + using base_t::logDebug; + using base_t::logError; + using base_t::logInfo; + using base_t::logTrace; + using base_t::logWarn; + + template + MccGenericMountNetworkServer(asio::io_context& ctx, MountT& mount, LoggerCtorArgsTs&&... log_args) + : base_t(ctx, {}, std::forward(log_args)...) + { + std::stringstream st; + st << std::this_thread::get_id(); + + logInfo(std::format("Create MccGenericMountNetworkServer class instance (thread ID = {})", st.str())); + + // to avoid possible compiler optimization (one needs to catch 'mount' strictly by reference) + auto* mount_ptr = &mount; + + base_t::_handleMessageFunc = [mount_ptr, this](std::string_view command) { + using mount_error_t = typename MountT::error_t; + mount_error_t m_err; + + auto nn = std::this_thread::get_id(); + + MccNetMessage input_msg; + using output_msg_t = MccNetMessage; + output_msg_t output_msg; + + auto ec = parseMessage(command, input_msg); + if (ec) { + output_msg.construct(MCC_COMMPROTO_KEYWORD_SERVER_ERROR_STR, ec); + } else { + output_msg = handleMessage(input_msg, mount_ptr); + } + + return output_msg.template byteRepr(); + }; + + // special functor (used in the destructor) + _stopMountFunc = [mount_ptr]() { mount_ptr->stopMount(); }; + } + + virtual ~MccGenericMountNetworkServer() + { + std::stringstream st; + st << std::this_thread::get_id(); + + _stopMountFunc(); + + logInfo(std::format("Delete MccGenericMountNetworkServer class instance (thread ID = {})", st.str())); + } + +protected: + MccCoordinateSerializer::SerializedCoordFormat _coordFormat{ + MccCoordinateSerializer::SerializedCoordFormat::CFMT_SGM}; + MccCoordinateSerializer::SexagesimalCoordPrec _coordPrec{2, 1}; + + std::function _stopMountFunc{}; + + template + RESULT_MSG_T handleMessage(const INPUT_MSG_T& input_msg, MountT* mount_ptr) + requires( + std::derived_from> && + std::derived_from>) + { + using mount_error_t = typename MountT::error_t; + mount_error_t m_err; + + RESULT_MSG_T output_msg; + + std::error_code err{}; + + if (input_msg.withKey(MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR)) { // strange! + output_msg.construct(MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR, input_msg.byteRepr()); + } else if (input_msg.withKey(MCC_COMMPROTO_KEYWORD_SERVER_ERROR_STR)) { // ??!!! + output_msg.construct(MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR, input_msg.byteRepr()); + } else if (input_msg.withKey(MCC_COMMPROTO_KEYWORD_RESTART_SERVER_STR)) { + this->restart(); + output_msg.construct(MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR, input_msg.byteRepr()); + } else if (input_msg.withKey(MCC_COMMPROTO_KEYWORD_INIT_STR)) { + m_err = mount_ptr->initMount(); + if (m_err) { + err = mcc_deduce_error_code(m_err, MccGenericMountNetworkServerErrorCode::ERROR_MOUNT_INIT); + } else { + output_msg.construct(MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR, MCC_COMMPROTO_KEYWORD_INIT_STR); + } + } else if (input_msg.withKey(MCC_COMMPROTO_KEYWORD_STOP_STR)) { + m_err = mount_ptr->stopMount(); + if (m_err) { + err = mcc_deduce_error_code(m_err, MccGenericMountNetworkServerErrorCode::ERROR_MOUNT_STOP); + } else { + output_msg.construct(MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR, MCC_COMMPROTO_KEYWORD_STOP_STR); + } + } else if (input_msg.withKey(MCC_COMMPROTO_KEYWORD_SLEW_STR)) { + m_err = mount_ptr->slewToTarget(false); + if (m_err) { + err = mcc_deduce_error_code(m_err, MccGenericMountNetworkServerErrorCode::ERROR_MOUNT_SLEW); + } else { + output_msg.construct(MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR, MCC_COMMPROTO_KEYWORD_SLEW_STR); + } + } else if (input_msg.withKey(MCC_COMMPROTO_KEYWORD_MOVE_STR)) { + m_err = mount_ptr->slewToTarget(true); + if (m_err) { + err = mcc_deduce_error_code(m_err, MccGenericMountNetworkServerErrorCode::ERROR_MOUNT_MOVE); + } else { + output_msg.construct(MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR, MCC_COMMPROTO_KEYWORD_MOVE_STR); + } + } else if (input_msg.withKey(MCC_COMMPROTO_KEYWORD_TRACK_STR)) { + m_err = mount_ptr->trackTarget(); + if (m_err) { + err = mcc_deduce_error_code(m_err, MccGenericMountNetworkServerErrorCode::ERROR_MOUNT_TRACK); + } else { + output_msg.construct(MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR, MCC_COMMPROTO_KEYWORD_TRACK_STR); + } + } else if (input_msg.withKey(MCC_COMMPROTO_KEYWORD_COORDFMT_STR)) { + auto v = input_msg.template paramValue(0); + if (v) { + _coordFormat = v.value(); + output_msg.construct(MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR, input_msg.byteRepr()); + } else { + err = v.error(); + } + } else if (input_msg.withKey(MCC_COMMPROTO_KEYWORD_COORDPREC_STR)) { + auto v = input_msg.template paramValue(0); + if (v) { + _coordPrec = v.value(); + output_msg.construct(MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR, input_msg.byteRepr()); + } else { + err = v.error(); + } + } else if (input_msg.withKey(MCC_COMMPROTO_KEYWORD_TARGET_STR)) { + // by default return ICRS coordinates + MccCelestialPoint cp{.pair_kind = MccCoordPairKind::COORDS_KIND_RADEC_ICRS}; + + auto sz = input_msg.paramSize(); + if (sz) { // set or get operation + auto vp = input_msg.template paramValue(0); // is it get operation? + if (vp) { // coordinate pair kind is given, it is get operation + cp.pair_kind = vp.value(); + err = coordsFromTelemetryData(*mount_ptr, true, cp); + if (!err) { + output_msg.construct(MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR, MCC_COMMPROTO_KEYWORD_TARGET_STR, + _coordFormat, _coordPrec, cp); + } + } else { // here a celestial point must be given + auto vc = input_msg.template paramValue(0); + if (vc) { // set operation + auto m_err = mount_ptr->setPointingTarget(vc.value()); + if (m_err) { + if (m_err) { + err = mcc_deduce_error_code( + m_err, MccGenericMountNetworkServerErrorCode::ERROR_MOUNT_SET_TARGET); + } + } else { + output_msg.construct(MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR, input_msg.byteRepr()); + } + } else { + err = vc.error(); + } + } + } else { // get operation + err = coordsFromTelemetryData(*mount_ptr, true, cp); + if (!err) { + output_msg.construct(MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR, MCC_COMMPROTO_KEYWORD_TARGET_STR, + _coordFormat, _coordPrec, cp); + } + } + + } else if (input_msg.withKey(MCC_COMMPROTO_KEYWORD_MOUNT_STR)) { + // by default return ICRS coordinates + MccCelestialPoint cp{.pair_kind = MccCoordPairKind::COORDS_KIND_RADEC_ICRS}; + + if (input_msg.paramSize()) { // ccordinate pair kind is given + auto vp = input_msg.template paramValue(0); + if (vp) { // coordinate pair kind is given + cp.pair_kind = vp.value(); + err = coordsFromTelemetryData(*mount_ptr, false, cp); + if (!err) { + output_msg.construct(MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR, MCC_COMMPROTO_KEYWORD_MOUNT_STR, + _coordFormat, _coordPrec, cp); + } + } else { // invalid command!!! + err = vp.error(); + } + + } else { + err = coordsFromTelemetryData(*mount_ptr, false, cp); + if (!err) { + output_msg.construct(MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR, MCC_COMMPROTO_KEYWORD_MOUNT_STR, + _coordFormat, _coordPrec, cp); + } + } + } else if (input_msg.withKey(MCC_COMMPROTO_KEYWORD_TELEMETRY_STR)) { + MccTelemetryData tdata; + + // auto t_err = mount_ptr->telemetryData(&tdata); + auto t_err = mount_ptr->waitForTelemetryData(&tdata); + if (t_err) { + err = mcc_deduce_error_code(t_err, MccGenericMountNetworkServerErrorCode::ERROR_MOUNT_GET_TELEMETRY); + } else { + output_msg.construct(MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR, MCC_COMMPROTO_KEYWORD_TELEMETRY_STR, + _coordFormat, _coordPrec, tdata); + } + } else if (input_msg.withKey(mcc::network::MCC_COMMPROTO_KEYWORD_STATUS_STR)) { + auto st = mount_ptr->mountStatus(); // according to mcc_generic_mount_c 'st' is formattable + using st_t = decltype(st); + if constexpr (std::is_enum_v) { + output_msg.construct(MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR, MCC_COMMPROTO_KEYWORD_STATUS_STR, + std::to_underlying(st)); + } else { + output_msg.construct(MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR, MCC_COMMPROTO_KEYWORD_STATUS_STR, + std::format(st)); + } + } else { + err = std::make_error_code(std::errc::invalid_argument); + } + + if (err) { // send error description + output_msg.construct(MCC_COMMPROTO_KEYWORD_SERVER_ERROR_STR, err); + } + + + return output_msg; + } + + + template + std::error_code parseMessage(std::string_view msg_bytes, MSG_T& msg) + { + auto ec = msg.fromCharRange(msg_bytes); + + if (ec != MSG_T::ERROR_OK) { + return std::make_error_code(std::errc::invalid_argument); + } + // return ec; + + return {}; + } + + + std::error_code coordsFromTelemetryData(mcc_generic_mount_c auto& mount, + bool target, // true - target coordinates, false - mount coordinates + mcc_celestial_point_c auto& cp) + { + MccTelemetryData tdata; + + auto t_err = mount.waitForTelemetryData(&tdata); + // auto t_err = mount.telemetryData(&tdata); + if (t_err) { + return mcc_deduce_error_code(t_err, MccGenericMountNetworkServerErrorCode::ERROR_MOUNT_GET_TELEMETRY); + } + + mcc_tp2tp(tdata.target.time_point, cp.time_point); + + switch (cp.pair_kind) { + case mcc::MccCoordPairKind::COORDS_KIND_RADEC_ICRS: + if (target) { + cp.X = tdata.target.RA_ICRS; + cp.Y = tdata.target.DEC_ICRS; + mcc_tp2tp(MccCelestialCoordEpoch::J2000_UTC, cp.time_point); + } // ??!!! + break; + case mcc::MccCoordPairKind::COORDS_KIND_RADEC_APP: + if (target) { + cp.X = tdata.target.RA_APP; + cp.Y = tdata.target.DEC_APP; + } else { + cp.X = tdata.RA_APP; + cp.Y = tdata.DEC_APP; + } + + break; + case mcc::MccCoordPairKind::COORDS_KIND_HADEC_APP: + if (target) { + cp.X = tdata.target.HA; + cp.Y = tdata.target.DEC_APP; + } else { + cp.X = tdata.HA; + cp.Y = tdata.DEC_APP; + } + break; + case mcc::MccCoordPairKind::COORDS_KIND_AZZD: + if (target) { + cp.X = tdata.target.AZ; + cp.Y = tdata.target.ZD; + } else { + cp.X = tdata.AZ; + cp.Y = tdata.ZD; + } + break; + case mcc::MccCoordPairKind::COORDS_KIND_AZALT: + if (target) { + cp.X = tdata.target.AZ; + cp.Y = tdata.target.ALT; + } else { + cp.X = tdata.AZ; + cp.Y = tdata.ALT; + } + break; + case mcc::MccCoordPairKind::COORDS_KIND_XY: + if (target) { + cp.X = tdata.target.X; + cp.Y = tdata.target.Y; + } else { + cp.X = tdata.X; + cp.Y = tdata.Y; + } + break; + default: + return std::make_error_code(std::errc::invalid_argument); + } + + return {}; + } +}; + +} // namespace mcc::network diff --git a/mcc_netserver_endpoint.h b/mcc_netserver_endpoint.h new file mode 100644 index 0000000..72c0c56 --- /dev/null +++ b/mcc_netserver_endpoint.h @@ -0,0 +1,512 @@ +#pragma once + +/* MOUNT CONTROL COMPONENTS LIBRARY */ + + + +/* NETWORK SERVER ENDPOINT CLASS IMPLEMENTATION */ + +#include +#include +#include +#include +#include +#include +#include + +#include "mcc_traits.h" + +namespace mcc::network +{ + +namespace utils +{ + +static constexpr bool mcc_char_range_compare(const traits::mcc_char_view auto& what, + const traits::mcc_char_view auto& where, + bool case_insensitive = false) +{ + if (std::ranges::size(what) == std::ranges::size(where)) { + if (case_insensitive) { + auto f = std::ranges::search(where, + std::views::transform(what, [](const char& ch) { return std::tolower(ch); })); + return !f.empty(); + } else { + auto f = std::ranges::search(where, what); + return !f.empty(); + } + } + + return false; +} + +} // namespace utils + +/* + * Very simple various protocols endpoint parser and holder class + * + * endpoint: proto_mark://host_name:port_num/path + * where "path" is optional for all non-local protocol kinds; + * + * for local kind of protocols the endpoint must be given as: + * local://stream/PATH + * local://seqpacket/PATH + * local://serial/PATH + * where 'stream' and 'seqpacket' "host_name"-field marks the + * stream-type and seqpacket-type UNIX domain sockets protocols; + * 'serial' marks a serial (RS232/485) protocol. + * here, possible "port_num" field is allowed but ignored. + * + * NOTE: "proto_mark" and "host_name" (for local kind) fields are parsed in case-insensitive manner! + * + * EXAMPLES: tcp://192.168.70.130:3131 + * local://serial/dev/ttyS1 + * local://seqpacket/tmp/BM70_SERVER_SOCK + * + * + */ + + +class MccNetServerEndpoint +{ +public: + static constexpr std::string_view protoHostDelim = "://"; + static constexpr std::string_view hostPortDelim = ":"; + static constexpr std::string_view portPathDelim = "/"; + + enum proto_id_t : uint8_t { + PROTO_ID_LOCAL, + PROTO_ID_SEQLOCAL, + PROTO_ID_SERLOCAL, + PROTO_ID_TCP, + PROTO_ID_TLS, + PROTO_ID_UNKNOWN + }; + + static constexpr std::string_view protoMarkLocal{"local"}; // UNIX domain + static constexpr std::string_view protoMarkTCP{"tcp"}; // TCP + static constexpr std::string_view protoMarkTLS{"tls"}; // TLS + + static constexpr std::array validProtoMarks{protoMarkLocal, protoMarkTCP, protoMarkTLS}; + + + static constexpr std::string_view localProtoTypeStream{"stream"}; // UNIX domain stream + static constexpr std::string_view localProtoTypeSeqpacket{"seqpacket"}; // UNIX domain seqpacket + static constexpr std::string_view localProtoTypeSerial{"serial"}; // serial (RS232/485) + + static constexpr std::array validLocalProtoTypes{localProtoTypeStream, localProtoTypeSeqpacket, + localProtoTypeSerial}; + + + template + MccNetServerEndpoint(const R& ept) + { + fromRange(ept); + } + + MccNetServerEndpoint(const MccNetServerEndpoint& other) + { + copyInst(other); + } + + MccNetServerEndpoint(MccNetServerEndpoint&& other) + { + moveInst(std::move(other)); + } + + virtual ~MccNetServerEndpoint() = default; + + + MccNetServerEndpoint& operator=(const MccNetServerEndpoint& other) + { + copyInst(other); + + return *this; + } + + + MccNetServerEndpoint& operator=(MccNetServerEndpoint&& other) + { + moveInst(std::move(other)); + + return *this; + } + + template + requires std::ranges::contiguous_range + bool fromRange(const R& ept) + { + _isValid = false; + + // at least 'ws://a' (proto, proto-host delimiter and at least a single character of hostname) + if (std::ranges::size(ept) < 6) { + return _isValid; + } + + if constexpr (std::is_array_v>) { + _endpoint = ept; + } else { + _endpoint.clear(); + std::ranges::copy(ept, std::back_inserter(_endpoint)); + } + + auto found = std::ranges::search(_endpoint, protoHostDelim); + if (found.empty()) { + return _isValid; + } + + + ssize_t idx; + if ((idx = checkProtoMark(std::string_view{_endpoint.begin(), found.begin()})) < 0) { + return _isValid; + } + + _proto = validProtoMarks[idx]; + + _host = std::string_view{found.end(), _endpoint.end()}; + + auto f1 = std::ranges::search(_host, portPathDelim); + // std::string_view port_sv; + if (f1.empty() && isLocal()) { // no path, but it is mandatory for 'local'! + return _isValid; + } else { + _host = std::string_view(_host.begin(), f1.begin()); + + _path = std::string_view(f1.end(), &*_endpoint.end()); + + f1 = std::ranges::search(_host, hostPortDelim); + if (f1.empty() && !isLocal()) { // no port, but it is mandatory for non-local! + return _isValid; + } + + _portView = std::string_view(f1.end(), _host.end()); + if (_portView.size()) { + _host = std::string_view(_host.begin(), f1.begin()); + + if (!isLocal()) { + // convert port string to int + auto end_ptr = _portView.data() + _portView.size(); + + auto [ptr, ec] = std::from_chars(_portView.data(), end_ptr, _port); + if (ec != std::errc() || ptr != end_ptr) { + return _isValid; + } + } else { // ignore for local + _port = -1; + } + } else { + _port = -1; + } + + if (isLocal()) { // check for special values + idx = 0; + if (std::ranges::any_of(validLocalProtoTypes, [&idx, this](const auto& el) { + bool ok = utils::mcc_char_range_compare(_host, el, true); + if (!ok) { + ++idx; + } + return ok; + })) { + _host = validLocalProtoTypes[idx]; + } else { + return _isValid; + } + } + } + + _isValid = true; + + return _isValid; + } + + + bool isValid() const + { + return _isValid; + } + + + auto endpoint() const + { + return _endpoint; + } + + template + R proto() const + { + return part(PROTO_PART); + } + + std::string_view proto() const + { + return proto(); + } + + template + R host() const + { + return part(HOST_PART); + } + + std::string_view host() const + { + return host(); + } + + int port() const + { + return _port; + } + + template + R portView() const + { + return part(PORT_PART); + } + + std::string_view portView() const + { + return portView(); + } + + template + R path(RR&& root_path) const + { + if (_path.empty()) { + if constexpr (traits::mcc_output_char_range) { + R res; + std::ranges::copy(std::forward(root_path), std::back_inserter(res)); + + return res; + } else { // can't add root path!!! + return part(PATH_PART); + } + } + + auto N = std::ranges::distance(root_path.begin(), root_path.end()); + + if (N) { + R res; + std::filesystem::path pt(root_path.begin(), root_path.end()); + + if (isLocal() && _path[0] == '\0') { + std::ranges::copy(std::string_view(" "), std::back_inserter(res)); + pt /= _path.substr(1); + std::ranges::copy(pt.string(), std::back_inserter(res)); + *res.begin() = '\0'; + } else { + pt /= _path; + std::ranges::copy(pt.string(), std::back_inserter(res)); + } + + return res; + } else { + return part(PATH_PART); + } + } + + template + std::string path(RR&& root_path) const + { + return path(std::forward(root_path)); + } + + template + R path() const + { + return part(PATH_PART); + } + + std::string_view path() const + { + return path(); + } + + + bool isLocal() const + { + return proto() == protoMarkLocal; + } + + bool isLocalStream() const + { + return host() == localProtoTypeStream; + } + + bool isLocalSerial() const + { + return host() == localProtoTypeSerial; + } + + bool isLocalSeqpacket() const + { + return host() == localProtoTypeSeqpacket; + } + + + bool isTCP() const + { + return proto() == protoMarkTCP; + } + + bool isTLS() const + { + return proto() == protoMarkTLS; + } + + + // add '\0' char (or replace special-meaning char/char-sequence) to construct UNIX abstract namespace + // endpoint path + template + MccNetServerEndpoint& makeAbstract(const T& mark = nullptr) + requires(traits::mcc_input_char_range || std::same_as, char> || + std::is_null_pointer_v>) + { + if (!(isLocalStream() || isLocalSeqpacket())) { // only local proto is valid! + return *this; + } + + if constexpr (std::is_null_pointer_v) { // just insert '\0' + auto it = _endpoint.insert(std::string::const_iterator(_path.begin()), '\0'); + _path = std::string_view(it, _endpoint.end()); + } else if constexpr (std::same_as, char>) { // replace a character (mark) + auto pos = std::distance(_endpoint.cbegin(), std::string::const_iterator(_path.begin())); + if (_endpoint[pos] == mark) { + _endpoint[pos] = '\0'; + } + } else { // replace a character range (mark) + if (std::ranges::equal(_path | std::views::take(std::ranges::size(mark), mark))) { + auto pos = std::distance(_endpoint.cbegin(), std::string::const_iterator(_path.begin())); + _endpoint.replace(pos, std::ranges::size(mark), 1, '\0'); + _path = std::string_view(_endpoint.begin() + pos, _endpoint.end()); + } + } + + return *this; + } + +protected: + std::string _endpoint; + std::string_view _proto, _host, _path, _portView; + int _port; + bool _isValid; + + + virtual ssize_t checkProtoMark(std::string_view proto_mark) + { + ssize_t idx = 0; + + // case-insensitive look-up + bool found = std::ranges::any_of(MccNetServerEndpoint::validProtoMarks, [&idx, &proto_mark](const auto& el) { + bool ok = utils::mcc_char_range_compare(proto_mark, el, true); + + if (!ok) { + ++idx; + } + + return ok; + }); + + return found ? idx : -1; + } + + enum EndpointPart { PROTO_PART, HOST_PART, PATH_PART, PORT_PART }; + + template + R part(EndpointPart what) const + { + R res; + + // if (!_isValid) { + // return res; + // } + + auto part = _proto; + + switch (what) { + case PROTO_PART: + part = _proto; + break; + case HOST_PART: + part = _host; + break; + case PATH_PART: + part = _path; + break; + case PORT_PART: + part = _portView; + break; + default: + break; + } + + if constexpr (std::ranges::view) { + return {part.begin(), part.end()}; + } else { + std::ranges::copy(part, std::back_inserter(res)); + } + + return res; + } + + void copyInst(const MccNetServerEndpoint& other) + { + if (&other != this) { + if (other._isValid) { + _isValid = other._isValid; + _endpoint = other._endpoint; + _proto = other._proto; + + std::iterator_traits::difference_type idx; + if (other.isLocal()) { // for 'local' host is one of static class constants + _host = other._host; + } else { + idx = std::distance(other._endpoint.c_str(), other._host.data()); + _host = std::string_view(_endpoint.c_str() + idx, other._host.size()); + } + + idx = std::distance(other._endpoint.c_str(), other._path.data()); + _path = std::string_view(_endpoint.c_str() + idx, other._path.size()); + + idx = std::distance(other._endpoint.c_str(), other._portView.data()); + _portView = std::string_view(_endpoint.c_str() + idx, other._portView.size()); + + _port = other._port; + } else { + _isValid = false; + _endpoint = std::string(); + _proto = std::string_view(); + _host = std::string_view(); + _path = std::string_view(); + _portView = std::string_view(); + _port = -1; + } + } + } + + + void moveInst(MccNetServerEndpoint&& other) + { + if (&other != this) { + if (other._isValid) { + _isValid = std::move(other._isValid); + _endpoint = std::move(other._endpoint); + _proto = other._proto; + _host = std::move(other._host); + _path = std::move(other._path); + _port = std::move(other._port); + _portView = std::move(other._portView); + } else { + _isValid = false; + _endpoint = std::string(); + _proto = std::string_view(); + _host = std::string_view(); + _path = std::string_view(); + _portView = std::string_view(); + _port = -1; + } + } + } +}; + +} // namespace mcc::network diff --git a/mcc_netserver_proto.h b/mcc_netserver_proto.h new file mode 100644 index 0000000..03f092c --- /dev/null +++ b/mcc_netserver_proto.h @@ -0,0 +1,821 @@ +#pragma once + +/* MOUNT CONTROL COMPONENTS LIBRARY */ + +/* BASIC NETWORK PROTOCOL DEFINITIONS */ + + + +#include +#include +#include "mcc_angle.h" +#include "mcc_defaults.h" +#include "mcc_generics.h" +#include "mcc_utils.h" + +namespace mcc::network +{ + +/* + * The network protocol is the ASCII-based, case-sensitive textual protocol. + * The "client-server" communication is performed through messages. + * The message is a minimal unit of this communication. + * The model of network communication is a simple "client-server" one, i.e., + * client asks - server responds. + * + * network communication message format: + * [[][][]...] + * + * where + * - mandatory message keyword (one or more ASCII symbols) + * + * + * e.g. + * "TARGET 12:23:45.56 00:32:21.978\n" + */ + + +/* low-level network message format definitions */ + +static constexpr std::string_view MCC_COMMPROTO_STOP_SEQ = "\n"; +static constexpr std::string_view MCC_COMMPROTO_KEYPARAM_DELIM_SEQ = " "; +static constexpr std::string_view MCC_COMMPROTO_PARAMPARAM_DELIM_SEQ = ";"; +static constexpr std::string_view MCC_COMMPROTO_RANGEPARAM_DELIM_SEQ = ","; + + +/* server special keywords */ + +static constexpr std::string_view MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR = "ACK"; // ACK +static constexpr std::string_view MCC_COMMPROTO_KEYWORD_SERVER_ERROR_STR = "ERROR"; // mount operational error +// pre-defined errors +static constexpr std::string_view MCC_COMMPROTO_SERVER_ERROR_INVKEY_STR = "INVKEY"; // invalid keyword +static constexpr std::string_view MCC_COMMPROTO_SERVER_ERROR_INVPAR_STR = "INVPAR"; // invalid parameter + + +/* server control keywords */ + +static constexpr std::string_view MCC_COMMPROTO_KEYWORD_RESTART_SERVER_STR = "RESTART"; // restart server + + +/* BELOW IS ONE OF THE PROTOCOL OPTIONS CORRESPONDING MCC_GENERIC_MOUNT_C CONCEPT */ + + +/* predefined parameters */ + +static constexpr std::string_view MCC_COMMPROTO_COORD_KIND_RADEC_ICRS = "RADEC_ICRS"; // ICRS RA and DEC +static constexpr std::string_view MCC_COMMPROTO_COORD_KIND_RADEC = "RADEC"; // apparent RA and DEC +static constexpr std::string_view MCC_COMMPROTO_COORD_KIND_HADEC = "HADEC"; // apparent HA and DEC +static constexpr std::string_view MCC_COMMPROTO_COORD_KIND_AZZD = "AZZD"; // azimuth and zenithal distance +static constexpr std::string_view MCC_COMMPROTO_COORD_KIND_AZALT = "AZALT"; // azimuth and altitude +static constexpr std::string_view MCC_COMMPROTO_COORD_KIND_XY = "XY"; // hardware (encoder) coordinates + + +// static constexpr MccCoordPairKind mcc_str2pairkind(std::string_view spair) +// { +// return spair == MCC_COMMPROTO_COORD_KIND_RADEC_ICRS ? MccCoordPairKind::COORDS_KIND_RADEC_ICRS +// : spair == MCC_COMMPROTO_COORD_KIND_RADEC ? MccCoordPairKind::COORDS_KIND_RADEC_APP +// : spair == MCC_COMMPROTO_COORD_KIND_HADEC ? MccCoordPairKind::COORDS_KIND_HADEC_APP +// : spair == MCC_COMMPROTO_COORD_KIND_AZZD ? MccCoordPairKind::COORDS_KIND_AZZD +// : spair == MCC_COMMPROTO_COORD_KIND_AZALT ? MccCoordPairKind::COORDS_KIND_AZALT +// : spair == MCC_COMMPROTO_COORD_KIND_XY ? MccCoordPairKind::COORDS_KIND_XY +// : MccCoordPairKind::COORDS_KIND_GENERIC; +// } + +template +static constexpr MccCoordPairKind mcc_str2pairkind(R&& spair) +{ + if constexpr (std::is_pointer_v>) { + return mcc_str2pairkind(std::string_view{spair}); + } + + const auto hash = mcc::utils::FNV1aHash(std::forward(spair)); + + return hash == mcc::utils::FNV1aHash(MCC_COMMPROTO_COORD_KIND_RADEC_ICRS) ? MccCoordPairKind::COORDS_KIND_RADEC_ICRS + : hash == mcc::utils::FNV1aHash(MCC_COMMPROTO_COORD_KIND_RADEC) ? MccCoordPairKind::COORDS_KIND_RADEC_APP + : hash == mcc::utils::FNV1aHash(MCC_COMMPROTO_COORD_KIND_HADEC) ? MccCoordPairKind::COORDS_KIND_HADEC_APP + : hash == mcc::utils::FNV1aHash(MCC_COMMPROTO_COORD_KIND_AZZD) ? MccCoordPairKind::COORDS_KIND_AZZD + : hash == mcc::utils::FNV1aHash(MCC_COMMPROTO_COORD_KIND_AZALT) ? MccCoordPairKind::COORDS_KIND_AZALT + : hash == mcc::utils::FNV1aHash(MCC_COMMPROTO_COORD_KIND_XY) ? MccCoordPairKind::COORDS_KIND_XY + : MccCoordPairKind::COORDS_KIND_GENERIC; +} + + +static constexpr std::string_view mcc_pairkind2str(MccCoordPairKind kind) +{ + return kind == MccCoordPairKind::COORDS_KIND_RADEC_ICRS ? MCC_COMMPROTO_COORD_KIND_RADEC_ICRS + : kind == MccCoordPairKind::COORDS_KIND_RADEC_APP ? MCC_COMMPROTO_COORD_KIND_RADEC + : kind == MccCoordPairKind::COORDS_KIND_HADEC_APP ? MCC_COMMPROTO_COORD_KIND_HADEC + : kind == MccCoordPairKind::COORDS_KIND_AZZD ? MCC_COMMPROTO_COORD_KIND_AZZD + : kind == MccCoordPairKind::COORDS_KIND_AZALT ? MCC_COMMPROTO_COORD_KIND_AZALT + : kind == MccCoordPairKind::COORDS_KIND_XY ? MCC_COMMPROTO_COORD_KIND_XY + : "UNKNOWN"; +} + + +/* keywords */ + +// NOTE: THE COORDINATES AND TIME-RELATED QUANTITIES CAN BE EXPRESSED IN THE TWO FORMATS: +// 1) fixed-point real number, e.g. 123.43987537359 or -0.09775 +// 2) sexagesimal number, e.g. 10:43:43.12 or -123:54:12.435 +// +// IN THE FIRST CASE ALL NUMBERS MUST BE INTERPRETATED AS DEGREES, +// IN THE SECOND CASE NUMBERS MUST BE INTERPRETATED ACCORDING TO ITS TYPE: +// ALL TIME-RELATED QUANTITIES AND RA/HA COORDINATES MUST BE EXPRESSED +// IN FORMAT 'HOURS:MINUTES:SECONDS', WHILE DEC/ALT/AZ/ZD COORDINATES MUST +// BE EXPRESSED AS '+/-DEGREES:ARCMINUTES:ARCSECONDS' +// +// USER-ENTERED (FROM NETWORK CLIENTS) COORDINATE PAIR CAN BE PROVIDED IN A MIXED FORM, I.E., +// 12.34436658678 10:32:11.432 or +// 10:32:11.432 12.34436658678 +// +// SERVER-RESPONDED COORDINATES ARE ALWAYS IN THE SAME FORMAT, SEXAGESIMAL OR FIXED-POINT +// + +// format of output coordinates: +// "COORDFMT FMT-type\n" +// e.g.: +// "COORDFMT SGM\n" +// "COORDFMT\n" +// +// server must return "ACK" or "ERROR INVPAR" in the case of 'set'-operation and +// "ACK COORDFMT FMT-type" in the case of 'get'-operation +// e.g.: +// "COORDFMT FIX\n" -> "ACK\n" +// "COORDFMT SXT\n" -> "ERROR INVPAR\n" (invalid parameter of format type) +// "COORDFMT\n" -> "ACK COORDFMT FIX\n" +// + +static constexpr std::string_view MCC_COMMPROTO_KEYWORD_COORDFMT_STR = "COORDFMT"; +static constexpr std::string_view MCC_COMMPROTO_KEYWORD_COORDFMT_SEXGM_STR = "SGM"; // sexagesimal +static constexpr std::string_view MCC_COMMPROTO_KEYWORD_COORDFMT_FIXED_STR = "FIX"; // fixed point + + +// precision (number of decimal places) of returned coordinates: +// "COORDPREC seconds-prec arcseconds-prec\n" +// seconds-prec - precision of hour-based coordinates (RA and HA) or time-related quantities +// arcseconds-prec - precision of degree-based coordinates (DEC, AZ, ZD, ALT) +// precision must be given as non-negative integer number +// e.g. +// "COORDPREC 2,1\n" (output sexagesimal RA=12:34:56.67, DEC=32:54:21.9) +// +static constexpr std::string_view MCC_COMMPROTO_KEYWORD_COORDPREC_STR = "COORDPREC"; + + +// set/get target coordinates +// "TARGET X-coord Y-coord XY-kind\n", if 'XY-kind' is omitted then one should assume RADEC_ICRS +// e.g.: +// "TARGET 12.7683487 10:23:09.75 AZZD\n" +// "TARGET HADEC\n" +// "TARGET\n" +// +// server must return "ACK" or "ERROR INVPAR" in the case of 'set'-operation and +// "ACK TARGET X-coord Y-coord XY-kind" in the case of 'get'-operation +// e.g.: +// "TARGET 12.7683487 10:23:09.75 AZZD\n" -> "ACK\n" +// "TARGET 12.7683487 10:23:09.75 AZZE\n" -> "ERROR INVPAR\n" (invalid parameter of coordinates pair kind) +// +// "TARGET HADEC\n" -> "ACK TARGET 20:21:56.32 -01:32:34.2 HADEC\n" +// "TARGET\n" -> "ACK TARGET 20:21:56.32 -01:32:34.2 RADEC_ICRS\n" +// + +static constexpr std::string_view MCC_COMMPROTO_KEYWORD_TARGET_STR = "TARGET"; + +// get mount coordinates: +// "MOUNT coord-kind", if 'coord-kind' is omitted then coordinates are according to mount type, +// i.e., HADEC for equathorial-type mount and AZZD for alt-azimuthal one +// e.g.: +// "MOUNT RADEC\n" (get current apparent RA and DEC mount coordinates) +// +// server must return "ACK MOUNT X-coord Y-coord XY-kind" or "ERROR INVPAR" +// e.g. +// "MOUNT AZALT\n" -> "ACK MOUNT 1.2332325 54.23321312 AZALT\n" +// "MOUNT AZAL\n" -> "ERROR INVPAR\n" (invalid parameter of coordinates pair kind) +// "MOUNT\n" -> "ACK MOUNT 1.2332325 54.23321312 AZZD\n" for alt-azimuthal mount +// "MOUNT\n" -> "ACK MOUNT 1.2332325 54.23321312 HADEC\n" for equathorial mount + +static constexpr std::string_view MCC_COMMPROTO_KEYWORD_MOUNT_STR = "MOUNT"; + + +static constexpr std::string_view MCC_COMMPROTO_KEYWORD_TELEMETRY_STR = "TELEMETRY"; + +// init mount +// "INIT\n" +static constexpr std::string_view MCC_COMMPROTO_KEYWORD_INIT_STR = "INIT"; + +// stop any movements +// "STOP\n" +static constexpr std::string_view MCC_COMMPROTO_KEYWORD_STOP_STR = "STOP"; + +// slew mount and track target: +// "SLEW\n" +static constexpr std::string_view MCC_COMMPROTO_KEYWORD_SLEW_STR = "SLEW"; + +// slew mount and stop: +// "MOVE\n" +static constexpr std::string_view MCC_COMMPROTO_KEYWORD_MOVE_STR = "MOVE"; + +// track target +// "TRACK\n" +static constexpr std::string_view MCC_COMMPROTO_KEYWORD_TRACK_STR = "TRACK"; + +// get mount status +// "STATUS\n" +static constexpr std::string_view MCC_COMMPROTO_KEYWORD_STATUS_STR = "STATUS"; + +// valid keywords +static constexpr std::array MCC_COMMPROTO_VALID_KEYS = { + MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR, MCC_COMMPROTO_KEYWORD_SERVER_ERROR_STR, MCC_COMMPROTO_KEYWORD_COORDFMT_STR, + MCC_COMMPROTO_KEYWORD_COORDPREC_STR, MCC_COMMPROTO_KEYWORD_TARGET_STR, MCC_COMMPROTO_KEYWORD_MOUNT_STR, + MCC_COMMPROTO_KEYWORD_TELEMETRY_STR, MCC_COMMPROTO_KEYWORD_INIT_STR, MCC_COMMPROTO_KEYWORD_STOP_STR, + MCC_COMMPROTO_KEYWORD_SLEW_STR, MCC_COMMPROTO_KEYWORD_MOVE_STR, MCC_COMMPROTO_KEYWORD_TRACK_STR, + MCC_COMMPROTO_KEYWORD_STATUS_STR}; + + +// hashes of valid keywords +static constexpr std::array MCC_COMMPROTO_VALID_KEYS_HASH = [](std::index_sequence) { + return std::array{mcc::utils::FNV1aHash(MCC_COMMPROTO_VALID_KEYS[Is])...}; +}(std::make_index_sequence()); + + + +template +concept mcc_netmsg_valid_keys_c = requires(T t) { + // std::array of valid message keywords + [](std::array) { + // to ensure T::NETMSG_VALID_KEYWORDS can be used as compile-time constant + static constexpr auto v0 = T::NETMSG_VALID_KEYWORDS[0]; + return v0; + }(T::NETMSG_VALID_KEYWORDS); + + // std::array of valid message keywords hashes + [](std::array) { + // to ensure T::NETMSG_VALID_KEYWORD_HASHES can be used as compile-time constant + static constexpr auto v0 = T::NETMSG_VALID_KEYWORD_HASHES[0]; + return v0; + }(T::NETMSG_VALID_KEYWORD_HASHES); + + requires T::NETMSG_VALID_KEYWORDS.size() == T::NETMSG_VALID_KEYWORD_HASHES.size(); +}; + + +struct MccNetMessageValidKeywords { + static constexpr std::array NETMSG_VALID_KEYWORDS = { + MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR, MCC_COMMPROTO_KEYWORD_SERVER_ERROR_STR, + MCC_COMMPROTO_KEYWORD_COORDFMT_STR, MCC_COMMPROTO_KEYWORD_COORDPREC_STR, + MCC_COMMPROTO_KEYWORD_TARGET_STR, MCC_COMMPROTO_KEYWORD_MOUNT_STR, + MCC_COMMPROTO_KEYWORD_TELEMETRY_STR, MCC_COMMPROTO_KEYWORD_INIT_STR, + MCC_COMMPROTO_KEYWORD_STOP_STR, MCC_COMMPROTO_KEYWORD_SLEW_STR, + MCC_COMMPROTO_KEYWORD_MOVE_STR, MCC_COMMPROTO_KEYWORD_TRACK_STR, + MCC_COMMPROTO_KEYWORD_STATUS_STR}; + + + // hashes of valid keywords + static constexpr std::array NETMSG_VALID_KEYWORD_HASHES = [](std::index_sequence) { + return std::array{mcc::utils::FNV1aHash(NETMSG_VALID_KEYWORDS[Is])...}; + }(std::make_index_sequence()); + + constexpr static const size_t* isKeywordValid(std::string_view key) + { + const auto hash = mcc::utils::FNV1aHash(key); + + for (auto const& h : NETMSG_VALID_KEYWORD_HASHES) { + if (h == hash) { + return &h; + } + } + + return nullptr; + } +}; + +static_assert(mcc_netmsg_valid_keys_c, ""); + + +template +concept mcc_netmessage_c = requires(T t) { T(); }; + + + +template +class MccNetMessage +{ +protected: + class DefaultDeserializer : protected mcc::utils::MccSimpleDeserializer + { + protected: + using base_t = mcc::utils::MccSimpleDeserializer; + + inline static mcc::MccCelestialPointDeserializer _cpDeserializer{MCC_COMMPROTO_RANGEPARAM_DELIM_SEQ}; + inline static mcc::MccEqtHrzCoordsDeserializer _eqhrDeserializer{MCC_COMMPROTO_RANGEPARAM_DELIM_SEQ}; + inline static mcc::MccTelemetryDataDeserializer _telemetryDeserializer{MCC_COMMPROTO_RANGEPARAM_DELIM_SEQ}; + + public: + DefaultDeserializer() : base_t(MCC_COMMPROTO_RANGEPARAM_DELIM_SEQ) {} + + template + std::error_code operator()(IR&& bytes, VT& value) const + { + if constexpr (mcc_telemetry_data_c) { + return _telemetryDeserializer(std::forward(bytes), value); + } else if constexpr (mcc_eqt_hrz_coord_c) { + return _eqhrDeserializer(std::forward(bytes), value); + } else if constexpr (mcc_celestial_point_c) { + return _cpDeserializer(std::forward(bytes), value); + } else if constexpr (std::same_as) { + value = MccCoordStrToPairKind(bytes); + if (value == MccCoordPairKind::COORDS_KIND_UNKNOWN) { + // return std::make_error_code(std::errc::invalid_argument); + return MccCoordinateConvErrorCode::ERROR_INVALID_CPAIR; + } + } else if constexpr (std::same_as) { + std::string v; + auto ec = (*this)(std::forward(bytes), v); + if (ec) { + return ec; + } + + if (v.compare(MCC_COMMPROTO_KEYWORD_COORDFMT_SEXGM_STR) == 0) { + value = MccCoordinateSerializer::SerializedCoordFormat::CFMT_SGM; + } else if (v.compare(MCC_COMMPROTO_KEYWORD_COORDFMT_FIXED_STR) == 0) { + value = MccCoordinateSerializer::SerializedCoordFormat::CFMT_DEGREES; + } else { + // return std::make_error_code(std::errc::invalid_argument); + return MccCoordinateConvErrorCode::ERROR_INVALID_COORD_FMT; + } + } else if constexpr (std::same_as) { + std::vector v; + auto ec = (*this)(std::forward(bytes), v); + if (ec) { + // return ec; + return MccCoordinateConvErrorCode::ERROR_INVALID_COORD_PREC; + } + + auto hprec = v[0]; + value.hour_prec = hprec > 0 ? (hprec < std::numeric_limits::max() + ? hprec + : std::numeric_limits::max()) + : 2; + if (v.size() == 1) { + value.deg_prec = 1; + } else { + auto dprec = v[1]; + value.deg_prec = dprec > 0 ? dprec < std::numeric_limits::max() + ? dprec + : std::numeric_limits::max() + : 1; + } + } else { + return base_t::operator()(std::forward(bytes), value); + } + + return {}; + } + }; + + + class DefaultSerializer + { + friend class MccNetMessage; + + MccCoordinateSerializer::SerializedCoordFormat _coordFmt{}; + MccCoordinateSerializer::SexagesimalCoordPrec _coordPrec{}; + + public: + template + void operator()(const T& value, OR& bytes) + { + if constexpr (std::is_arithmetic_v) { + std::format_to(std::back_inserter(bytes), "{}", value); + } else if constexpr (std::convertible_to) { + std::ranges::copy(static_cast(value), std::back_inserter(bytes)); + } else if constexpr (std::constructible_from) { + std::ranges::copy(std::string(value), std::back_inserter(bytes)); + } else if constexpr (traits::mcc_char_range) { + std::ranges::copy(std::string(value.begin(), value.end()), std::back_inserter(bytes)); + // } else if constexpr (std::same_as) { + // std::ranges::copy(mcc_pairkind2str(value), std::back_inserter(bytes)); + } else if constexpr (traits::mcc_time_duration_c) { + (*this)(value.count(), bytes); + } else if constexpr (mcc_telemetry_data_c) { + static MccTelemetryDataSerializer sr; + + sr.setDelimiter(MCC_COMMPROTO_RANGEPARAM_DELIM_SEQ); + sr.setFormat(_coordFmt); + sr.setPrecision(_coordPrec); + + sr(value, bytes); + } else if constexpr (mcc_eqt_hrz_coord_c) { + static MccEqtHrzCoordsSerializer sr; + + sr.setDelimiter(MCC_COMMPROTO_RANGEPARAM_DELIM_SEQ); + sr.setFormat(_coordFmt); + sr.setPrecision(_coordPrec); + + sr(value, bytes); + } else if constexpr (mcc_celestial_point_c) { + static MccCelestialPointSerializer sr; + + sr.setDelimiter(MCC_COMMPROTO_RANGEPARAM_DELIM_SEQ); + sr.setFormat(_coordFmt); + sr.setPrecision(_coordPrec); + + sr(value, bytes); + } else if constexpr (std::ranges::range) { + auto sz = std::ranges::size(value); + if (sz == 0) { + return; + } + + (*this)(*value.begin(), bytes); // the first element + + if (sz > 1) { + for (auto const& el : value | std::views::drop(1)) { + std::ranges::copy(MCC_COMMPROTO_RANGEPARAM_DELIM_SEQ, std::back_inserter(bytes)); + (*this)(el, bytes); + } + } + } else if constexpr (std::same_as) { + std::format_to(std::back_inserter(bytes), "{}{}{}{}{}", value.value(), + MCC_COMMPROTO_RANGEPARAM_DELIM_SEQ, value.message(), MCC_COMMPROTO_RANGEPARAM_DELIM_SEQ, + value.category().name()); + } else if constexpr (std::formattable) { + std::format_to(std::back_inserter(bytes), "{}", value); + } else { + static_assert(false, "UNSUPPORTED TYPE!!!"); + } + } + }; + + +public: + typedef BASE_T valid_keys_t; + typedef BYTEREPR_T byte_repr_t; + + enum MccNetMessageError { ERROR_OK, ERROR_EMPTY_MESSAGE, ERROR_INVALID_KEYWORD, ERROR_EMPTY_KEYWORD }; + + MccNetMessage() = default; + + template + MccNetMessage(KT&& key, PTs&&... params) + requires traits::mcc_output_char_range + { + construct(_defaultSerializer, std::forward(key), std::forward(params)...); + } + + template + constexpr MccNetMessage(const R& msg) + requires traits::mcc_input_char_range + { + fromCharRange(msg); + } + + // constexpr MccNetMessage(const BYTEREPR_T& msg) + // requires traits::mcc_input_char_range + // { + // fromCharRange(msg); + // } + + virtual ~MccNetMessage() = default; + + template + constexpr bool withKey(const KT& key) const + { + if constexpr (std::is_pointer_v>) { + return withKey(std::string_view{key}); + } + + return mcc::utils::FNV1aHash(key) == _keywordHash; + } + + + template + R keyword() const + { + if constexpr (traits::mcc_char_view) { + return R{_keyword.begin(), _keyword.end()}; + } else { + R r; + std::ranges::copy(_keyword, std::back_inserter(r)); + + return r; + } + } + + std::string_view keyword() const + { + return _keyword; + } + + + size_t paramSize() const + { + return _params.size(); + } + + template + R params(size_t start_idx = 0, size_t Nelemes = std::numeric_limits::max()) const + requires(traits::mcc_view_or_output_char_range || traits::mcc_range_of_char_range) + { + if (start_idx >= _params.size()) { + return R{}; + } + + auto stop_idx = start_idx + Nelemes - 1; + if (stop_idx >= _params.size()) { + stop_idx = _params.size() - 1; + } + + if constexpr (traits::mcc_range_of_char_range) { // returm parameters as array + using el_t = std::ranges::range_value_t; + + R r; + if constexpr (traits::mcc_char_view || traits::mcc_output_char_range) { + for (size_t i = start_idx; i <= stop_idx; ++i) { + auto& el = _params[i]; + std::back_inserter(r) = el_t{el.begin(), el.end()}; + } + } else { + static_assert(false, "UNSUPPORTED RANGE TYPE!!!"); + } + + return r; + } else { + if constexpr (traits::mcc_char_view) { // return joined parameters as a single char-range + return R{_params[start_idx].begin(), _params[stop_idx].end()}; + } else { + R r; + std::ranges::copy(std::string_view{_params[start_idx].begin(), _params[stop_idx].end()}, + std::back_inserter(r)); + + return r; + } + } + } + + std::string_view params(size_t start_idx = 0, size_t Nelemes = std::numeric_limits::max()) const + { + return params(start_idx, Nelemes); + } + + template + R param(size_t idx) const + { + if (idx >= _params.size()) { + return {}; + } + + if constexpr (traits::mcc_char_view) { + return R{_params[idx].begin(), _params[idx].end()}; + } else { + R r; + std::ranges::copy(_params[idx], std::back_inserter(r)); + + return r; + } + } + + std::string_view param(size_t idx) const + { + if (idx >= _params.size()) { + return {}; + } + + return _params[idx]; + } + + + template + std::expected paramValue(size_t idx, DeserFuncT&& deser_func) const + { + if (idx >= _params.size()) { + return std::unexpected{std::make_error_code(std::errc::value_too_large)}; + } + + T val; + + auto ec = std::forward(deser_func)(_params[idx], val); + if (ec) { + return std::unexpected(ec); + } else { + return val; + } + } + + template + std::expected paramValue(size_t idx) const + { + return paramValue(idx, _defaultDeserializer); + } + + + template + R byteRepr() const + { + if constexpr (traits::mcc_char_view) { + return R{_msgBuffer.begin(), _msgBuffer.end()}; + } else { + R r; + std::ranges::copy(_msgBuffer, std::back_inserter(r)); + + return r; + } + } + + std::string_view byteRepr() const + { + return byteRepr(); + } + + template + std::error_code construct(KT&& key, PTs&&... params) + requires traits::mcc_output_char_range + { + return construct(_defaultSerializer, std::forward(key), std::forward(params)...); + } + + // + // serializing function SerFuncT - a callable with the signature: + // template + // void ser_func(const T& val, R&& buffer) + // + template + std::error_code construct(SerFuncT&& ser_func, KT&& key, PTs&&... params) + requires(traits::mcc_output_char_range && + !traits::mcc_input_char_range>) + { + if constexpr (std::is_pointer_v>) { + return construct(std::forward(ser_func), std::string_view(key), std::forward(params)...); + } + + + if (!std::ranges::size(key)) { + return std::make_error_code(std::errc::invalid_argument); + } + + auto r = valid_keys_t::isKeywordValid(key); + if (!r) { + return std::make_error_code(std::errc::not_supported); + } + + _keywordHash = *r; + + _msgBuffer = BYTEREPR_T{}; + + std::ranges::copy(std::forward(key), std::back_inserter(_msgBuffer)); + // _keyword = {_msgBuffer.begin(), _msgBuffer.end()}; + size_t key_idx = std::distance(_msgBuffer.begin(), _msgBuffer.end()); + std::vector par_idx; + + _params.clear(); + + if constexpr (sizeof...(PTs)) { + std::ranges::copy(MCC_COMMPROTO_KEYPARAM_DELIM_SEQ, std::back_inserter(_msgBuffer)); + + convertFunc(std::forward(ser_func), par_idx, std::forward(params)...); + + for (size_t i = 0; i < par_idx.size(); i += 2) { + _params.emplace_back(_msgBuffer.begin() + par_idx[i], _msgBuffer.begin() + par_idx[i + 1]); + } + } + + _keyword = std::string_view{_msgBuffer.begin(), _msgBuffer.begin() + key_idx}; + + return {}; + } + + template + constexpr MccNetMessageError fromCharRange(const R& r) + { + if constexpr (std::is_pointer_v>) { + return fromCharRange(std::string_view(r)); + } + + + if (std::ranges::size(r) == 0) { + return ERROR_EMPTY_MESSAGE; + } + + std::string_view key; + + // auto prev_msg_buff = _msgBuffer; + + if constexpr (traits::mcc_output_char_range) { + _msgBuffer = BYTEREPR_T{}; + std::ranges::copy(r, std::back_inserter(_msgBuffer)); + } else { + _msgBuffer = {std::begin(r), std::end(r)}; + } + + auto found = std::ranges::search(_msgBuffer, MCC_COMMPROTO_KEYPARAM_DELIM_SEQ); + + if (found.empty()) { // only keyword + key = mcc::utils::trimSpaces(std::string_view{_msgBuffer.begin(), _msgBuffer.end()}); + } else { + key = mcc::utils::trimSpaces(std::string_view{_msgBuffer.begin(), found.begin()}); + } + + auto kv = valid_keys_t::isKeywordValid(key); + if (!kv) { + // _msgBuffer = prev_msg_buff; // restore previous netmessage state + return ERROR_INVALID_KEYWORD; + } + + _keywordHash = *kv; + _keyword = key; + + if (!found.empty()) { // params ... + _params.clear(); + auto pr = + std::views::split(std::string_view{found.end(), _msgBuffer.end()}, MCC_COMMPROTO_PARAMPARAM_DELIM_SEQ); + for (auto const& p : pr) { + _params.emplace_back(p.begin(), p.end()); + } + } + + return ERROR_OK; + } + +protected: + size_t _keywordHash{}; + std::string_view _keyword{}; + std::vector _params{}; + + BYTEREPR_T _msgBuffer{}; + + inline static DefaultDeserializer _defaultDeserializer{}; + + DefaultSerializer _defaultSerializer{}; + + template + void convertFunc(std::vector& idx, const T& par, const Ts&... pars) + { + if constexpr (std::same_as) { + _defaultSerializer._coordFmt = par; + if constexpr (sizeof...(Ts)) { + convertFunc(idx, pars...); + } + } else if constexpr (std::same_as) { + _defaultSerializer._coordPrec = par; + if constexpr (sizeof...(Ts)) { + convertFunc(idx, pars...); + } + } else { + convertFunc(_defaultSerializer, idx, par, pars...); + // idx.emplace_back(std::distance(_msgBuffer.begin(), _msgBuffer.end())); + + // _defaultSerializer(par, _msgBuffer); + + // idx.emplace_back(std::distance(_msgBuffer.begin(), _msgBuffer.end())); + + // if constexpr (sizeof...(Ts)) { + // std::ranges::copy(MCC_COMMPROTO_PARAMPARAM_DELIM_SEQ, std::back_inserter(_msgBuffer)); + + // convertFunc(idx, pars...); + // } + } + }; + + template + void convertFunc(SerFuncT&& ser_func, std::vector& idx, const T& par, const Ts&... pars) + requires(!std::same_as, std::vector>) + { + if constexpr (std::derived_from, DefaultSerializer>) { + if constexpr (std::same_as) { + // _defaultSerializer._coordFmt = par; + ser_func._coordFmt = par; + } else if constexpr (std::same_as) { + _defaultSerializer._coordPrec = par; + } else { + idx.emplace_back(std::distance(_msgBuffer.begin(), _msgBuffer.end())); + + std::forward(ser_func)(par, _msgBuffer); + + idx.emplace_back(std::distance(_msgBuffer.begin(), _msgBuffer.end())); + + if constexpr (sizeof...(Ts)) { + std::ranges::copy(MCC_COMMPROTO_PARAMPARAM_DELIM_SEQ, std::back_inserter(_msgBuffer)); + } + } + } else { + idx.emplace_back(std::distance(_msgBuffer.begin(), _msgBuffer.end())); + + std::forward(ser_func)(par, _msgBuffer); + + idx.emplace_back(std::distance(_msgBuffer.begin(), _msgBuffer.end())); + + if constexpr (sizeof...(Ts)) { + std::ranges::copy(MCC_COMMPROTO_PARAMPARAM_DELIM_SEQ, std::back_inserter(_msgBuffer)); + } + } + + if constexpr (sizeof...(Ts)) { + convertFunc(std::forward(ser_func), idx, pars...); + } + } +}; + +static_assert(MccNetMessage{"ACK"}.withKey("ACK")); +static_assert(MccNetMessage{"ACK"}.withKey("ACK")); + +} // namespace mcc::network