From 0902156b9642eb474d21c770196adf6131b97821 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 11 Oct 2023 15:55:17 +0200 Subject: [PATCH 01/25] Refactored project structure and updated build system Significantly revised the project structure and made updates to the conanfile.py and CMakeLists.txt. The project was adjusted to follow a more standard layout, with all public headers moved to a central 'include' directory. The build system was updated to use more modern CMake practices and use the Conan package manager more effectively, removing the previous manual download of conan.cmake. Additionally, several source files were moved or renamed to support the changes in organization and convention. Extended the .gitignore file to ignore more unnecessary files created by the build system and IDE, and updated various #include directives in the source code to reflect the new file locations. The test setup in CMakeLists.txt was also simplified and improved for better integration with Google Test. These changes should provide a cleaner and more maintainable project structure, simplify the build process, and reduce potential for errors. It should also reduce the learning curve for new developers looking to contribute to the project. Contributes to CURA-10561 --- .gitignore | 11 +- CMakeLists.txt | 59 ++++--- apps/CMakeLists.txt | 6 - apps/translator_main.cpp | 3 - conanfile.py | 147 +++++++++++++++--- include/{ => dulcificum}/command_types.h | 1 + .../miracle_jtp}/mgjtp_command_to_json.h | 4 +- .../miracle_jtp}/mgjtp_json_to_command.h | 2 +- .../mgjtp_mappings_json_key_to_str.h | 2 +- src/CMakeLists.txt | 13 -- src/command_types.cpp | 2 +- .../mgjtp_command_to_json.cpp | 4 +- .../mgjtp_json_to_command.cpp | 4 +- .../CMakeLists.txt | 8 - test/CMakeLists.txt | 19 ++- test/miracle_jsontoolpath_dialect_test.cpp | 4 +- test/test_data_dir.h | 11 ++ test/test_data_dir.h.in | 10 -- 18 files changed, 200 insertions(+), 110 deletions(-) delete mode 100644 apps/CMakeLists.txt delete mode 100644 apps/translator_main.cpp rename include/{ => dulcificum}/command_types.h (99%) rename {src/miraclegrue_jsontoolpath_dialect => include/dulcificum/miracle_jtp}/mgjtp_command_to_json.h (77%) rename {src/miraclegrue_jsontoolpath_dialect => include/dulcificum/miracle_jtp}/mgjtp_json_to_command.h (78%) rename {src/miraclegrue_jsontoolpath_dialect => include/dulcificum/miracle_jtp}/mgjtp_mappings_json_key_to_str.h (97%) delete mode 100644 src/CMakeLists.txt rename src/{miraclegrue_jsontoolpath_dialect => miracle_jtp}/mgjtp_command_to_json.cpp (96%) rename src/{miraclegrue_jsontoolpath_dialect => miracle_jtp}/mgjtp_json_to_command.cpp (96%) delete mode 100644 src/miraclegrue_jsontoolpath_dialect/CMakeLists.txt create mode 100644 test/test_data_dir.h delete mode 100644 test/test_data_dir.h.in diff --git a/.gitignore b/.gitignore index f0e67d3..5d24095 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,13 @@ *.exe *.out *.app -/test/test_data_dir.h + +build +.idea + +CMakeUserPresets.json +conan.lock +conanbuildinfo.txt +conaninfo.txt +graph_info.json + diff --git a/CMakeLists.txt b/CMakeLists.txt index 7ea8fe9..9f78ec1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,30 +1,41 @@ -cmake_minimum_required(VERSION 3.16) +cmake_minimum_required(VERSION 3.23) project(dulcificum) -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_POSITION_INDEPENDENT_CODE ON) +find_package(standardprojectsettings REQUIRED) +option(EXTENSIVE_WARNINGS "Build with all warnings" ON) +option(ENABLE_TESTS "Build with unit test" ON) -#------------------------------# -# SETUP CONAN -#------------------------------# -if (NOT EXISTS "${CMAKE_BINARY_DIR}/conan.cmake") - message(STATUS "Downloading conan.cmake from https://github.com/conan-io/cmake-conan") - file(DOWNLOAD "https://raw.githubusercontent.com/conan-io/cmake-conan/master/conan.cmake" - "${CMAKE_BINARY_DIR}/conan.cmake") -endif () +find_package(nlohmann_json REQUIRED) -include(${CMAKE_BINARY_DIR}/conan.cmake) +# --- Setup the shared C++ mgjtp library --- +set(DULCIFICUM_SRC + src/command_types.cpp + src/miracle_jtp/mgjtp_command_to_json.cpp + src/miracle_jtp/mgjtp_json_to_command.cpp +) +add_library(dulcificum ${DULCIFICUM_SRC}) +target_link_libraries(dulcificum PUBLIC nlohmann_json::nlohmann_json) -conan_cmake_run(CONANFILE conanfile.py - BASIC_SETUP TARGETS - BUILD missing - # KEEP_RPATHS +target_include_directories(dulcificum + PUBLIC + $ + $ ) -set(CMAKE_MODULE_PATH ${CMAKE_BINARY_DIR} ${CMAKE_MODULE_PATH}) - -#------------------------------# -# SUBDIRECTORIES -#------------------------------# -add_subdirectory(apps) -add_subdirectory(src) -add_subdirectory(test) \ No newline at end of file + +use_threads(dulcificum) +enable_sanitizers(dulcificum) +if (${EXTENSIVE_WARNINGS}) + set_project_warnings(dulcificum) +endif () + + +# --- Setup command-line utility --- is this still needed + + +# --- Setup Python bindings --- + + +# --- Setup tests +if (ENABLE_TESTS) + add_subdirectory(test) +endif () \ No newline at end of file diff --git a/apps/CMakeLists.txt b/apps/CMakeLists.txt deleted file mode 100644 index 988735f..0000000 --- a/apps/CMakeLists.txt +++ /dev/null @@ -1,6 +0,0 @@ -add_executable(translator - translator_main.cpp -) -target_link_libraries(translator dulcificum) -target_include_directories(translator PUBLIC "${PROJECT_SOURCE_DIR}/include") -target_include_directories(translator PUBLIC "${PROJECT_SOURCE_DIR}/src") diff --git a/apps/translator_main.cpp b/apps/translator_main.cpp deleted file mode 100644 index e9cdae1..0000000 --- a/apps/translator_main.cpp +++ /dev/null @@ -1,3 +0,0 @@ -int main() { - return 0; -} \ No newline at end of file diff --git a/conanfile.py b/conanfile.py index 27f7752..93b3074 100644 --- a/conanfile.py +++ b/conanfile.py @@ -1,34 +1,133 @@ import os -from conans import ConanFile, CMake +from conan import ConanFile +from conan.errors import ConanInvalidConfiguration +from conan.tools.build import check_min_cppstd +from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain, cmake_layout +from conan.tools.env import VirtualBuildEnv +from conan.tools.files import copy, mkdir, AutoPackager +from conan.tools.microsoft import check_min_vs, is_msvc_static_runtime, is_msvc +from conan.tools.scm import Version -class DiscreteGeoKernelConan(ConanFile): - settings = "os", "compiler", "build_type", "arch" - requires = [ - "gtest/1.12.1", - "nlohmann_json/3.11.2", - ] - generators = "cmake" - options = {} - default_options = {} +required_conan_version = ">=1.58.0 <2.0.0" + + +class DulcificumConan(ConanFile): + name = "dulcificum" + description = "Dulcificum changes the flavor, or dialect, of 3d printer commands" + author = "UltiMaker" + license = "" + url = "https://github.com/Ultimaker/synsepalum-dulcificum" + homepage = "https://ultimaker.com" + topics = ("cura", "curaengine", "gcode-generation", "3D-printing", "miraclegrue", "toolpath") + package_type = "library" + settings = "os", "arch", "compiler", "build_type" + options = { + "shared": [True, False], + "fPIC": [True, False], + "enable_extensive_warnings": [True, False], + } + default_options = { + "shared": False, + "fPIC": True, + "enable_extensive_warnings": False, + } + + def set_version(self): + if not self.version: + self.version = "0.1.0-alpha" + + @property + def _min_cppstd(self): + return 20 + + @property + def _compilers_minimum_version(self): + return { + "gcc": "11", + "clang": "14", + "apple-clang": "13", + "msvc": "192", + "visual_studio": "17", + } + + def export_sources(self): + copy(self, "CMakeLists.txt", self.recipe_folder, self.export_sources_folder) + copy(self, "*", os.path.join(self.recipe_folder, "src"), os.path.join(self.export_sources_folder, "src")) + copy(self, "*", os.path.join(self.recipe_folder, "include"), os.path.join(self.export_sources_folder, "include")) + copy(self, "*", os.path.join(self.recipe_folder, "test"), os.path.join(self.export_sources_folder, "test")) + + def config_options(self): + if self.settings.os == "Windows": + del self.options.fPIC + + def configure(self): + if self.options.shared: + self.options.rm_safe("fPIC") + + def layout(self): + cmake_layout(self) + self.cpp.package.libs = ["dulcificum"] + + def requirements(self): + self.requires("nlohmann_json/3.11.2", transitive_headers = True) + + def build_requirements(self): + self.test_requires("standardprojectsettings/[>=0.1.0]@ultimaker/stable") + if not self.conf.get("tools.build:skip_test", False, check_type = bool): + self.test_requires("gtest/[>=1.12.1]") + + def validate(self): + if self.settings.compiler.cppstd: + check_min_cppstd(self, self._min_cppstd) + check_min_vs(self, 191) + if not is_msvc(self): + minimum_version = self._compilers_minimum_version.get(str(self.settings.compiler), False) + if minimum_version and Version(self.settings.compiler.version) < minimum_version: + raise ConanInvalidConfiguration( + f"{self.ref} requires C++{self._min_cppstd}, which your compiler does not support." + ) + if is_msvc(self) and self.options.shared: + raise ConanInvalidConfiguration(f"{self.ref} can not be built as shared on Visual Studio and msvc.") + + def generate(self): + tc = CMakeToolchain(self) + tc.variables["ENABLE_TESTS"] = not self.conf.get("tools.build:skip_test", False, check_type = bool) + tc.variables["EXTENSIVE_WARNINGS"] = self.options.enable_extensive_warnings + if is_msvc(self): + tc.variables["USE_MSVC_RUNTIME_LIBRARY_DLL"] = not is_msvc_static_runtime(self) + tc.cache_variables["CMAKE_POLICY_DEFAULT_CMP0077"] = "NEW" + tc.generate() + + tc = CMakeDeps(self) + tc.generate() + + tc = VirtualBuildEnv(self) + tc.generate(scope = "build") + + for dep in self.dependencies.values(): + if len(dep.cpp_info.libdirs) > 0: + copy(self, "*.dylib", dep.cpp_info.libdirs[0], self.build_folder) + copy(self, "*.dll", dep.cpp_info.libdirs[0], self.build_folder) + if len(dep.cpp_info.bindirs) > 0: + copy(self, "*.dll", dep.cpp_info.bindirs[0], self.build_folder) + if not self.conf.get("tools.build:skip_test", False, check_type = bool): + test_path = os.path.join(self.build_folder, "tests") + if not os.path.exists(test_path): + mkdir(self, test_path) + if len(dep.cpp_info.libdirs) > 0: + copy(self, "*.dylib", dep.cpp_info.libdirs[0], os.path.join(self.build_folder, "tests")) + copy(self, "*.dll", dep.cpp_info.libdirs[0], os.path.join(self.build_folder, "tests")) + if len(dep.cpp_info.bindirs) > 0: + copy(self, "*.dll", dep.cpp_info.bindirs[0], os.path.join(self.build_folder, "tests")) def build(self): cmake = CMake(self) - cmake.verbose() cmake.configure() cmake.build() - def requirements(self): - pass - - def imports(self): - self.copy("*.h", dst="include", src="src") - self.copy("*.lib", dst="lib", keep_path=False) - if "cmake_multi" in self.generators: - self.copy("*.dll", dst="bin/" + str(self.settings.build_type), keep_path=False) - else: - self.copy("*.dll", dst="bin", keep_path=False) - self.copy("*.dylib*", dst="lib", keep_path=False) - self.copy("*.so*", dst="lib", keep_path=False) - self.copy("*.a", dst="lib", keep_path=False) + def package(self): + copy(self, pattern="LICENSE", dst=os.path.join(self.package_folder, "licenses"), src=self.source_folder) + packager = AutoPackager(self) + packager.run() diff --git a/include/command_types.h b/include/dulcificum/command_types.h similarity index 99% rename from include/command_types.h rename to include/dulcificum/command_types.h index e9cf09a..50dc5d3 100644 --- a/include/command_types.h +++ b/include/dulcificum/command_types.h @@ -4,6 +4,7 @@ #include #include #include +#include namespace dulcificum { diff --git a/src/miraclegrue_jsontoolpath_dialect/mgjtp_command_to_json.h b/include/dulcificum/miracle_jtp/mgjtp_command_to_json.h similarity index 77% rename from src/miraclegrue_jsontoolpath_dialect/mgjtp_command_to_json.h rename to include/dulcificum/miracle_jtp/mgjtp_command_to_json.h index 7f441ad..3d20076 100644 --- a/src/miraclegrue_jsontoolpath_dialect/mgjtp_command_to_json.h +++ b/include/dulcificum/miracle_jtp/mgjtp_command_to_json.h @@ -1,9 +1,9 @@ #ifndef DULCIFICUM_MIRACLEGRUE_JSONTOOLPATH_H_ #define DULCIFICUM_MIRACLEGRUE_JSONTOOLPATH_H_ -#include "nlohmann/json.hpp" +#include -#include "../../include/command_types.h" +#include "dulcificum/command_types.h" namespace dulcificum::miracle_jtp { diff --git a/src/miraclegrue_jsontoolpath_dialect/mgjtp_json_to_command.h b/include/dulcificum/miracle_jtp/mgjtp_json_to_command.h similarity index 78% rename from src/miraclegrue_jsontoolpath_dialect/mgjtp_json_to_command.h rename to include/dulcificum/miracle_jtp/mgjtp_json_to_command.h index 875b05f..f14d366 100644 --- a/src/miraclegrue_jsontoolpath_dialect/mgjtp_json_to_command.h +++ b/include/dulcificum/miracle_jtp/mgjtp_json_to_command.h @@ -1,7 +1,7 @@ #ifndef DULCIFICUM_MIRACLE_GRUE_JSONTOOLPATH_IN_H #define DULCIFICUM_MIRACLE_GRUE_JSONTOOLPATH_IN_H -#include "mgjtp_mappings_json_key_to_str.h" +#include namespace dulcificum::miracle_jtp { diff --git a/src/miraclegrue_jsontoolpath_dialect/mgjtp_mappings_json_key_to_str.h b/include/dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h similarity index 97% rename from src/miraclegrue_jsontoolpath_dialect/mgjtp_mappings_json_key_to_str.h rename to include/dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h index 3275d9b..85d9340 100644 --- a/src/miraclegrue_jsontoolpath_dialect/mgjtp_mappings_json_key_to_str.h +++ b/include/dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h @@ -1,7 +1,7 @@ #ifndef DULCIFICUM_MIRACLE_GRUE_JSONTOOLPATH_JSON_KEYS_H #define DULCIFICUM_MIRACLE_GRUE_JSONTOOLPATH_JSON_KEYS_H -#include "mgjtp_command_to_json.h" +#include "dulcificum/miracle_jtp/mgjtp_command_to_json.h" namespace dulcificum { diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt deleted file mode 100644 index 085bc74..0000000 --- a/src/CMakeLists.txt +++ /dev/null @@ -1,13 +0,0 @@ -add_subdirectory(miraclegrue_jsontoolpath_dialect) -list(TRANSFORM MIRACLEGRUE_TOOLPATH_DIALECT_SRC PREPEND miraclegrue_jsontoolpath_dialect/) - -add_library(dulcificum - command_types.cpp - ../include/command_types.h - ${MIRACLEGRUE_TOOLPATH_DIALECT_SRC} -) - -target_include_directories(dulcificum PUBLIC miraclegrue_jsontoolpath_dialect) -target_include_directories(dulcificum INTERFACE ${PROJECT_BINARY_DIR}/include) - -target_link_libraries(dulcificum PUBLIC ${CONAN_LIBS}) diff --git a/src/command_types.cpp b/src/command_types.cpp index 2df3029..124f4d7 100644 --- a/src/command_types.cpp +++ b/src/command_types.cpp @@ -1,4 +1,4 @@ -#include "../include/command_types.h" +#include "dulcificum/command_types.h" namespace dulcificum::botcmd { CommandPtr spawnCommandPtr(CommandType type) { diff --git a/src/miraclegrue_jsontoolpath_dialect/mgjtp_command_to_json.cpp b/src/miracle_jtp/mgjtp_command_to_json.cpp similarity index 96% rename from src/miraclegrue_jsontoolpath_dialect/mgjtp_command_to_json.cpp rename to src/miracle_jtp/mgjtp_command_to_json.cpp index f594d36..a9cb6dd 100644 --- a/src/miraclegrue_jsontoolpath_dialect/mgjtp_command_to_json.cpp +++ b/src/miracle_jtp/mgjtp_command_to_json.cpp @@ -1,5 +1,5 @@ -#include "mgjtp_command_to_json.h" -#include "mgjtp_mappings_json_key_to_str.h" +#include "dulcificum/miracle_jtp/mgjtp_command_to_json.h" +#include "dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h" namespace dulcificum::miracle_jtp { using namespace botcmd; diff --git a/src/miraclegrue_jsontoolpath_dialect/mgjtp_json_to_command.cpp b/src/miracle_jtp/mgjtp_json_to_command.cpp similarity index 96% rename from src/miraclegrue_jsontoolpath_dialect/mgjtp_json_to_command.cpp rename to src/miracle_jtp/mgjtp_json_to_command.cpp index 39a9e44..98531c5 100644 --- a/src/miraclegrue_jsontoolpath_dialect/mgjtp_json_to_command.cpp +++ b/src/miracle_jtp/mgjtp_json_to_command.cpp @@ -1,5 +1,5 @@ -#include "mgjtp_command_to_json.h" -#include "mgjtp_mappings_json_key_to_str.h" +#include "dulcificum/miracle_jtp/mgjtp_command_to_json.h" +#include "dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h" namespace dulcificum::miracle_jtp { using namespace botcmd; diff --git a/src/miraclegrue_jsontoolpath_dialect/CMakeLists.txt b/src/miraclegrue_jsontoolpath_dialect/CMakeLists.txt deleted file mode 100644 index 687f291..0000000 --- a/src/miraclegrue_jsontoolpath_dialect/CMakeLists.txt +++ /dev/null @@ -1,8 +0,0 @@ -set( - MIRACLEGRUE_TOOLPATH_DIALECT_SRC - mgjtp_command_to_json.cpp - mgjtp_command_to_json.h - mgjtp_json_to_command.cpp - mgjtp_json_to_command.h - PARENT_SCOPE -) \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 5115cd3..cc66997 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,17 +1,16 @@ -set(DULCIFICUM_TEST_DATA_DIR "${CMAKE_SOURCE_DIR}/test/data") -configure_file(test_data_dir.h.in "${CMAKE_SOURCE_DIR}/test/test_data_dir.h") +message(STATUS "dulcificum: Compiling with Tests") +enable_testing() -file(GLOB UNIT_TEST_FILES *.cpp) +find_package(GTest REQUIRED) + +set(UNIT_TEST_FILES + dulcificum_unit_tester.cpp + miracle_jsontoolpath_dialect_test.cpp +) add_executable(dulcificum_unit_tester ${UNIT_TEST_FILES} test_data_dir.h ) -target_include_directories( - dulcificum_unit_tester PUBLIC - "${PROJECT_SOURCE_DIR}/src" - "${PROJECT_SOURCE_DIR}/src/miraclegrue_jsontoolpath_dialect" -) - -target_link_libraries(dulcificum_unit_tester dulcificum ${CONAN_LIBS}) \ No newline at end of file +target_link_libraries(dulcificum_unit_tester PUBLIC dulcificum GTest::gtest GTest::gmock) \ No newline at end of file diff --git a/test/miracle_jsontoolpath_dialect_test.cpp b/test/miracle_jsontoolpath_dialect_test.cpp index 839874a..eeec991 100644 --- a/test/miracle_jsontoolpath_dialect_test.cpp +++ b/test/miracle_jsontoolpath_dialect_test.cpp @@ -1,7 +1,7 @@ #include "gtest/gtest.h" -#include "mgjtp_json_to_command.h" -#include "mgjtp_command_to_json.h" +#include "dulcificum/miracle_jtp/mgjtp_json_to_command.h" +#include "dulcificum/miracle_jtp/mgjtp_command_to_json.h" #include #include "test_data_dir.h" diff --git a/test/test_data_dir.h b/test/test_data_dir.h new file mode 100644 index 0000000..334b9d2 --- /dev/null +++ b/test/test_data_dir.h @@ -0,0 +1,11 @@ +#ifndef DULCIFICUM_TEST_DATA_DIR_H +#define DULCIFICUM_TEST_DATA_DIR_H + +#include +#include + +namespace dulcificum { + static const auto kTestDataDir = std::filesystem::path { std::source_location::current().file_name() }.parent_path().append("data"); +} + +#endif //DULCIFICUM_TEST_DATA_DIR_H diff --git a/test/test_data_dir.h.in b/test/test_data_dir.h.in deleted file mode 100644 index 2a30837..0000000 --- a/test/test_data_dir.h.in +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef DULCIFICUM_TEST_DATA_DIR_H -#define DULCIFICUM_TEST_DATA_DIR_H - -#include - -namespace dulcificum { - static const std::filesystem::path kTestDataDir = "${DULCIFICUM_TEST_DATA_DIR}"; -} - -#endif //DULCIFICUM_TEST_DATA_DIR_H From 12702943f8519a8ff774eb3fbd302c11391648de Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 11 Oct 2023 16:06:47 +0200 Subject: [PATCH 02/25] Add Conan package workflow and update unit tests This update adds a new GitHub workflow for managing Conan packages. The workflow exports the recipe, sources, and binaries for Mac, Windows, and Linux and uploads them to the server where they can be used downstream. It also includes a new requirements file for the Conan package. The unit test workflow has been updated to include steps for installing dependencies and publishing test results. This will ensure consistent building and testing environments. Contributes to CURA-10561 --- .github/workflows/conan-package.yml | 141 +++++++++++++++++ .../workflows/requirements-conan-package.txt | 1 + .github/workflows/unit-test.yml | 144 ++++++++++++++++++ 3 files changed, 286 insertions(+) create mode 100644 .github/workflows/conan-package.yml create mode 100644 .github/workflows/requirements-conan-package.txt create mode 100644 .github/workflows/unit-test.yml diff --git a/.github/workflows/conan-package.yml b/.github/workflows/conan-package.yml new file mode 100644 index 0000000..c5e9230 --- /dev/null +++ b/.github/workflows/conan-package.yml @@ -0,0 +1,141 @@ +--- +name: conan-package + +# Exports the recipe, sources and binaries for Mac, Windows and Linux and upload these to the server such that these can +# be used downstream. +# +# It should run on pushes against main or CURA-* branches, but it will only create the binaries for main and release branches + +on: + workflow_dispatch: + inputs: + # FIXME: Not yet implemented + conan_id: + required: false + type: string + description: 'The full conan package ID, e.g. "dulcificum/1.2.3@ultimaker/stable"' + create_latest_alias: + required: true + default: false + type: boolean + description: 'Create latest alias' + create_binaries_windows: + required: true + default: false + type: boolean + description: 'create binaries Windows' + create_binaries_linux: + required: true + default: false + type: boolean + description: 'create binaries Linux' + create_binaries_macos: + required: true + default: false + type: boolean + description: 'create binaries Macos' + + push: + paths: + - 'include/**' + - 'src/**' + - 'test/**' + - 'test_package/**' + - 'conanfile.py' + - 'CMakeLists.txt' + - '.github/workflows/conan-package.yml' + - '.github/worflows/requirements-conan-package.txt' + branches: + - main + - 'CURA-*' + - '[0-9].[0-9]*' + - '[0-9].[0-9][0-9]*' + tags: + - '[0-9]+.[0-9]+.[0-9]*' + - '[0-9]+.[0-9]+.[0-9]' + +jobs: + conan-recipe-version: + uses: ultimaker/cura/.github/workflows/conan-recipe-version.yml@main + with: + project_name: dulcificum + + conan-package-export: + needs: [ conan-recipe-version ] + uses: ultimaker/cura/.github/workflows/conan-recipe-export.yml@main + with: + recipe_id_full: ${{ needs.conan-recipe-version.outputs.recipe_id_full }} + recipe_id_latest: ${{ needs.conan-recipe-version.outputs.recipe_id_latest }} + runs_on: 'ubuntu-22.04' + python_version: '3.11.x' + conan_logging_level: 'info' + secrets: inherit + + conan-package-create-macos: + if: ${{ (github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || needs.conan-recipe-version.outputs.is_release_branch == 'true')) || (github.event_name == 'workflow_dispatch' && inputs.create_binaries_macos) }} + needs: [ conan-recipe-version, conan-package-export ] + + uses: ultimaker/cura/.github/workflows/conan-package-create.yml@main + with: + project_name: ${{ needs.conan-recipe-version.outputs.project_name }} + recipe_id_full: ${{ needs.conan-recipe-version.outputs.recipe_id_full }} + build_id: 3 + runs_on: 'macos-11' + python_version: '3.11.x' + conan_logging_level: 'info' + secrets: inherit + + conan-package-create-windows: + if: ${{ (github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || needs.conan-recipe-version.outputs.is_release_branch == 'true' )) || (github.event_name == 'workflow_dispatch' && inputs.create_binaries_windows) }} + needs: [ conan-recipe-version, conan-package-export ] + + uses: ultimaker/cura/.github/workflows/conan-package-create.yml@main + with: + project_name: ${{ needs.conan-recipe-version.outputs.project_name }} + recipe_id_full: ${{ needs.conan-recipe-version.outputs.recipe_id_full }} + build_id: 4 + runs_on: 'windows-2022' + python_version: '3.11.x' + conan_config_branch: '' + conan_logging_level: 'info' + secrets: inherit + + conan-package-create-linux: + if: ${{ (github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || needs.conan-recipe-version.outputs.is_release_branch == 'true')) || (github.event_name == 'workflow_dispatch' && inputs.create_binaries_linux) }} + needs: [ conan-recipe-version, conan-package-export ] + + uses: ultimaker/cura/.github/workflows/conan-package-create.yml@main + with: + project_name: ${{ needs.conan-recipe-version.outputs.project_name }} + recipe_id_full: ${{ needs.conan-recipe-version.outputs.recipe_id_full }} + build_id: 2 + runs_on: 'ubuntu-22.04' + python_version: '3.11.x' + conan_logging_level: 'info' + secrets: inherit + + notify-export: + if: ${{ always() }} + needs: [ conan-recipe-version, conan-package-export ] + + uses: ultimaker/cura/.github/workflows/notify.yml@main + with: + success: ${{ contains(join(needs.*.result, ','), 'success') }} + success_title: "New Conan recipe exported in ${{ github.repository }}" + success_body: "Exported ${{ needs.conan-recipe-version.outputs.recipe_id_full }}" + failure_title: "Failed to export Conan Export in ${{ github.repository }}" + failure_body: "Failed to exported ${{ needs.conan-recipe-version.outputs.recipe_id_full }}" + secrets: inherit + + notify-create: + if: ${{ always() && ((github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || needs.conan-recipe-version.outputs.is_release_branch == 'true')) || (github.event_name == 'workflow_dispatch' && inputs.create_binaries_linux)) }} + needs: [ conan-recipe-version, conan-package-create-macos, conan-package-create-windows, conan-package-create-linux ] + + uses: ultimaker/cura/.github/workflows/notify.yml@main + with: + success: ${{ contains(join(needs.*.result, ','), 'success') }} + success_title: "New binaries created in ${{ github.repository }}" + success_body: "Created binaries for ${{ needs.conan-recipe-version.outputs.recipe_id_full }}" + failure_title: "Failed to create binaries in ${{ github.repository }}" + failure_body: "Failed to created binaries for ${{ needs.conan-recipe-version.outputs.recipe_id_full }}" + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/requirements-conan-package.txt b/.github/workflows/requirements-conan-package.txt new file mode 100644 index 0000000..b9e41b2 --- /dev/null +++ b/.github/workflows/requirements-conan-package.txt @@ -0,0 +1 @@ +conan>=1.60.2,<2.0.0 \ No newline at end of file diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml new file mode 100644 index 0000000..e5d9d82 --- /dev/null +++ b/.github/workflows/unit-test.yml @@ -0,0 +1,144 @@ +--- +name: unit-test +on: + push: + paths: + - 'include/**' + - 'src/**' + - 'cmake/**' + - 'test/**' + - 'conanfile.py' + - 'CMakeLists.txt' + - '.github/workflows/unit-test.yml' + - '.github/workflows/requirements-conan-package.txt' + branches: + - main + - '[0-9]+.[0-9]+' + tags: + - '[0-9]+.[0-9]+.[0-9]+' + pull_request: + types: [ opened, reopened, synchronize ] + paths: + - 'include/**' + - 'src/**' + - 'cmake/**' + - 'tests/**' + - 'test_package/**' + - 'conanfile.py' + - 'CMakeLists.txt' + - '.github/workflows/unit-test.yml' + - '.github/workflows/requirements-conan-package.txt' + branches: + - main + - 'CURA-*' + - '[0-9]+.[0-9]+' + tags: + - '[0-9]+.[0-9]+.[0-9]+' + +env: + CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }} + CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }} + CONAN_LOGIN_USERNAME_CURA_CE: ${{ secrets.CONAN_USER }} + CONAN_PASSWORD_CURA_CE: ${{ secrets.CONAN_PASS }} + CONAN_LOG_RUN_TO_OUTPUT: 1 + CONAN_LOGGING_LEVEL: info + CONAN_NON_INTERACTIVE: 1 + +jobs: + conan-recipe-version: + uses: ultimaker/cura/.github/workflows/conan-recipe-version.yml@main + with: + project_name: dulcificum + + testing: + runs-on: ubuntu-22.04 + needs: [ conan-recipe-version ] + + steps: + - name: Checkout dulcificum + uses: actions/checkout@v3 + + - name: Setup Python and pip + uses: actions/setup-python@v4 + with: + python-version: '3.11.x' + architecture: 'x64' + cache: 'pip' + cache-dependency-path: .github/workflows/requirements-conan-package.txt + + - name: Install Python requirements and Create default Conan profile + run: | + pip install -r .github/workflows/requirements-conan-package.txt + + # NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest. + # This is maybe because grub caches the disk it uses last time, which is recreated each time. + - name: Install Linux system requirements + if: ${{ runner.os == 'Linux' }} + run: | + sudo rm /var/cache/debconf/config.dat + sudo dpkg --configure -a + sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y + sudo apt update + sudo apt upgrade + sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config -y + + - name: Install GCC-132 on ubuntu + run: | + sudo apt install g++-13 gcc-13 -y + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 13 + sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 13 + + - name: Create the default Conan profile + run: conan profile new default --detect + + - name: Get Conan configuration + run: | + conan config install https://github.com/Ultimaker/conan-config.git + conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}" + + - name: Use Conan download cache (Bash) + if: ${{ runner.os != 'Windows' }} + run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache" + + - name: Cache Conan local repository packages (Bash) + uses: actions/cache@v3 + if: ${{ runner.os != 'Windows' }} + with: + path: | + $HOME/.conan/data + $HOME/.conan/conan_download_cache + key: conan-${{ runner.os }}-${{ runner.arch }} + + - name: Install dependencies + run: conan install . ${{ needs.conan-recipe-version.outputs.recipe_id_full }} -s build_type=Release --build=missing --update -g GitHubActionsRunEnv -g GitHubActionsBuildEnv + + - name: Upload the Dependency package(s) + run: conan upload "*" -r cura --all -c + + - name: Set Environment variables from Conan install (bash) + if: ${{ runner.os != 'Windows' }} + run: | + . ./activate_github_actions_runenv.sh + . ./activate_github_actions_buildenv.sh + working-directory: build/Release/generators + + - name: Build dulcificum and tests + run: | + cmake --preset release + cmake --build --preset release + + - name: Run Unit Test dulcificum + id: run-test + run: ctest --output-junit engine_test.xml + working-directory: build/Release + + - name: Publish Unit Test Results + id: test-results + uses: EnricoMi/publish-unit-test-result-action@v1 + if: ${{ always() }} + with: + files: | + **/*.xml + + - name: Conclusion + run: echo "Conclusion is ${{ fromJSON( steps.test-results.outputs.json ).conclusion }}" \ No newline at end of file From c07f2da419500179720327573c6d1a8dd5416b5e Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 11 Oct 2023 17:24:08 +0200 Subject: [PATCH 03/25] Add linting and formatting workflows for code quality checks This commit adds several GitHub workflows for automatic linting and formatting of the codebase. First, a "lint-formatter" workflow is added, which uses clang-format to automatically format .h, .cpp, and .h* files upon push. Then, a "lint-poster" workflow is added to handle displaying linting results after the completion of a "lint-tidier" workflow. Furthermore, a "lint-tidier" workflow applies the clang-tidy tool for automatic linting on pull requests for .h, .cpp, and .h* files. A python requirements file is included for the linter workflows, and standard clang configuration files for formatting and linting are also added. This change ensures consistent code styling and alerts for potential issues, which helps maintain the quality and readability of the codebase. Contributes to CURA-10561 --- .clang-format | 102 +++++++++++++++++++ .clang-tidy | 55 +++++++++++ .github/workflows/lint-formatter.yml | 48 +++++++++ .github/workflows/lint-poster.yml | 81 +++++++++++++++ .github/workflows/lint-tidier.yml | 114 ++++++++++++++++++++++ .github/workflows/requirements-linter.txt | 2 + 6 files changed, 402 insertions(+) create mode 100644 .clang-format create mode 100644 .clang-tidy create mode 100644 .github/workflows/lint-formatter.yml create mode 100644 .github/workflows/lint-poster.yml create mode 100644 .github/workflows/lint-tidier.yml create mode 100644 .github/workflows/requirements-linter.txt diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..bb491a8 --- /dev/null +++ b/.clang-format @@ -0,0 +1,102 @@ +AccessModifierOffset: -4 +AlignAfterOpenBracket: AlwaysBreak +AlignConsecutiveAssignments: None +AlignConsecutiveDeclarations: None +AlignEscapedNewlines: DontAlign +AlignOperands: AlignAfterOperator +AlignTrailingComments: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowAllArgumentsOnNextLine: false +AllowShortBlocksOnASingleLine: Empty +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterClass: true + AfterControlStatement: Always + AfterEnum: false + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: false + AfterStruct: true + AfterUnion: true + BeforeCatch: true + BeforeElse: true + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyNamespace: true + SplitEmptyRecord: true +BreakAfterJavaFieldAnnotations: true +BreakBeforeBinaryOperators: All +BreakBeforeBraces: Allman +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeComma +BreakStringLiterals: true +ColumnLimit: 180 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +PackConstructorInitializers: Never +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: false +DerivePointerAlignment: false +PointerAlignment: Left +DisableFormat: false +ExperimentalAutoDetectBinPacking: true +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Regroup +IncludeCategories: + - Priority: 2 + Regex: ^<(scripta|spdlog|range|fmt|Arcus|agrpc|grpc|boost)/ + - Priority: 3 + Regex: ^(<|"(gtest|gmock|isl|json)/) + - Priority: 1 + Regex: .* +IncludeIsMainRegex: (Test)?$ +IndentCaseLabels: false +IndentWidth: 4 +IndentWrappedFunctionNames: true +JavaScriptQuotes: Double +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +Language: Cpp +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 2 +NamespaceIndentation: None +ObjCBlockIndentWidth: 4 +ObjCSpaceAfterProperty: true +ObjCSpaceBeforeProtocolList: false +ReflowComments: true +SortIncludes: CaseSensitive +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: true +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: Never +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: true +SpacesInLineCommentPrefix: + Minimum: 1 + Maximum: -1 +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: c++20 +TabWidth: 4 +UseTab: Never \ No newline at end of file diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..e516426 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,55 @@ +Checks: > + *, + -llvmlibc-*, + -modernize-use-trailing-return-type, + -altera-unroll-loops*, + -readability-avoid-const-params-in-decls, + -fuchsia-default-arguments-calls, + -google-readability-todo, + -altera-struct-pack-align, + -android-*, + -misc-non-private-member-variables-in-classes, + -fuchsia-overloaded-operator, + -cppcoreguidelines-avoid-capturing-lambda-coroutines, + -llvm-header-guard, + -bugprone-easily-swappable-parameters +WarningsAsErrors: '-*' +HeaderFilterRegex: '' +FormatStyle: none +CheckOptions: + - key: google-build-namespaces.HeaderFileExtensions + value: h + - key: readability-function-size.LineThreshold + value: 100 + - key: readability-function-size.BranchThreshold + value: 10 + - key: readability-function-size.ParameterThreshold + value: 6 + - key: readability-function-size.NestingThreshold + value: 4 + - key: readability-identifier-naming.NamespaceCase + value: lower_case + - key: readability-identifier-naming.MacroDefinitionCase + value: UPPER_CASE + - key: readability-identifier-naming.ClassCase + value: CamelCase + - key: readability-identifier-naming.FunctionCase + value: camelBack + - key: readability-identifier-naming.MethodCase + value: camelBack + - key: readability-identifier-naming.ParameterCase + value: lower_case + - key: readability-identifier-naming.VariableCase + value: lower_case + - key: readability-identifier-naming.ClassConstantCase + value: UPPER_CASE + - key: readability-identifier-naming.GlobalConstantCase + value: lower_case + - key: readability-identifier-naming.GlobalVariableCase + value: UPPER_CASE + - key: readability-identifier-length.IgnoredParameterNames + value: 'p|p0|p1|i|j|k|x|X|y|Y|z|Z|a|A|b|B|c|C|d|D|ab|AB|ba|BA|bc|BC|cb|CB|cd|CD|dc|DC|ad|AD|da|DA|ip|os' + - key: readability-identifier-length.IgnoredVariableNames + value: '_p|p0|p1|i|j|k|x|X|y|Y|z|Z|a|A|b|B|c|C|d|D|ab|AB|ba|BA|bc|BC|cb|CB|cd|CD|dc|DC|ad|AD|da|DA|ip|os' + - key: readability-identifier-length.IgnoredLoopCounterNames + value: '_p|p0|p1|i|j|k|x|X|y|Y|z|Z|a|A|b|B|c|C|d|D|ab|AB|ba|BA|bc|BC|cb|CB|cd|CD|dc|DC|ad|AD|da|DA|ip|os' \ No newline at end of file diff --git a/.github/workflows/lint-formatter.yml b/.github/workflows/lint-formatter.yml new file mode 100644 index 0000000..d605e3f --- /dev/null +++ b/.github/workflows/lint-formatter.yml @@ -0,0 +1,48 @@ +name: lint-formatter + +on: + workflow_dispatch: + + push: + paths: + - 'include/**/*.h*' + - 'src/**/*.cpp*' + - 'test/**/*.cpp*' + +jobs: + lint-formatter-job: + name: Auto-apply clang-format + + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - uses: technote-space/get-diff-action@v6 + with: + PATTERNS: | + include/**/*.h* + src/**/*.cpp* + test/**/*.cpp* + + - name: Setup Python and pip + if: env.GIT_DIFF && !env.MATCHED_FILES # If nothing happens with python and/or pip after, the clean-up crashes. + uses: actions/setup-python@v4 + with: + python-version: 3.11.x + cache: 'pip' + cache-dependency-path: .github/workflows/requirements-linter.txt + + - name: Install Python requirements for runner + if: env.GIT_DIFF && !env.MATCHED_FILES + run: pip install -r .github/workflows/requirements-linter.txt + + - name: Format file + if: env.GIT_DIFF && !env.MATCHED_FILES + run: | + clang-format -i ${{ env.GIT_DIFF_FILTERED }} + + - uses: stefanzweifel/git-auto-commit-action@v4 + if: env.GIT_DIFF && !env.MATCHED_FILES + with: + commit_message: "Applied clang-format." \ No newline at end of file diff --git a/.github/workflows/lint-poster.yml b/.github/workflows/lint-poster.yml new file mode 100644 index 0000000..039a1c3 --- /dev/null +++ b/.github/workflows/lint-poster.yml @@ -0,0 +1,81 @@ +name: lint-poster + +on: + workflow_run: + workflows: [ "lint-tidier" ] + types: [ completed ] + +jobs: + lint-poster-job: + # Trigger the job only if the previous (insecure) workflow completed successfully + if: ${{ github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + steps: + - name: Download analysis results + uses: actions/github-script@v3.1.0 + with: + script: | + let artifacts = await github.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{github.event.workflow_run.id }}, + }); + let matchArtifact = artifacts.data.artifacts.filter((artifact) => { + return artifact.name == "linter-result" + })[0]; + let download = await github.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: "zip", + }); + let fs = require("fs"); + fs.writeFileSync("${{github.workspace}}/linter-result.zip", Buffer.from(download.data)); + + - name: Set environment variables + run: | + mkdir linter-result + unzip linter-result.zip -d linter-result + echo "pr_id=$(cat linter-result/pr-id.txt)" >> $GITHUB_ENV + echo "pr_head_repo=$(cat linter-result/pr-head-repo.txt)" >> $GITHUB_ENV + echo "pr_head_ref=$(cat linter-result/pr-head-ref.txt)" >> $GITHUB_ENV + + - uses: actions/checkout@v3 + with: + repository: ${{ env.pr_head_repo }} + ref: ${{ env.pr_head_ref }} + persist-credentials: false + + - name: Redownload analysis results + uses: actions/github-script@v3.1.0 + with: + script: | + let artifacts = await github.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{github.event.workflow_run.id }}, + }); + let matchArtifact = artifacts.data.artifacts.filter((artifact) => { + return artifact.name == "linter-result" + })[0]; + let download = await github.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: "zip", + }); + let fs = require("fs"); + fs.writeFileSync("${{github.workspace}}/linter-result.zip", Buffer.from(download.data)); + + - name: Extract analysis results + run: | + mkdir linter-result + unzip linter-result.zip -d linter-result + + - name: Run clang-tidy-pr-comments action + uses: platisd/clang-tidy-pr-comments + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + clang_tidy_fixes: linter-result/fixes.yml + pull_request_id: ${{ env.pr_id }} + request_changes: true \ No newline at end of file diff --git a/.github/workflows/lint-tidier.yml b/.github/workflows/lint-tidier.yml new file mode 100644 index 0000000..7fc316c --- /dev/null +++ b/.github/workflows/lint-tidier.yml @@ -0,0 +1,114 @@ +name: lint-tidier + +on: + workflow_dispatch: + + pull_request: + paths: + - 'include/**/*.h*' + - 'src/**/*.cpp*' + - 'test/**/*.cpp*' + +jobs: + conan-recipe-version: + uses: ultimaker/cura/.github/workflows/conan-recipe-version.yml@main + with: + project_name: dulcificum + + lint-tidier-job: + name: Auto-apply clang-tidy + + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - uses: technote-space/get-diff-action@v6 + with: + PATTERNS: | + include/**/*.h* + src/**/*.cpp* + test/**/*.cpp* + + - name: Setup Python and pip + if: env.GIT_DIFF && !env.MATCHED_FILES # If nothing happens with python and/or pip after, the clean-up crashes. + uses: actions/setup-python@v4 + with: + python-version: 3.11.x + cache: "pip" + cache-dependency-path: .github/workflows/requirements-linter.txt + + - name: Install Python requirements for runner + if: env.GIT_DIFF && !env.MATCHED_FILES + run: pip install -r .github/workflows/requirements-linter.txt + + # NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest. + # This is maybe because grub caches the disk it uses last time, which is recreated each time. + - name: Install Linux system requirements + if: ${{ runner.os == 'Linux' }} + run: | + sudo rm /var/cache/debconf/config.dat + sudo dpkg --configure -a + sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y + sudo apt update + sudo apt upgrade + sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config -y + + - name: Install GCC-132 on ubuntu + run: | + sudo apt install g++-13 gcc-13 -y + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 13 + sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 13 + + - name: Create the default Conan profile + run: conan profile new default --detect + + - name: Get Conan configuration + run: | + conan config install https://github.com/Ultimaker/conan-config.git + conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}" + + - name: Install dependencies + run: conan install . ${{ needs.conan-recipe-version.outputs.recipe_id_full }} -o enable_testing=True -s build_type=Release --build=missing --update -g GitHubActionsRunEnv -g GitHubActionsBuildEnv + + - name: Set Environment variables from Conan install (bash) + if: ${{ runner.os != 'Windows' }} + run: | + . ./activate_github_actions_runenv.sh + . ./activate_github_actions_buildenv.sh + working-directory: build/Release/generators + + - name: Build CuraEngine and tests + run: | + cmake --preset release + cmake --build --preset release + + - name: Create results directory + run: mkdir linter-result + + - name: Diagnose file(s) + if: env.GIT_DIFF && !env.MATCHED_FILES + continue-on-error: true + run: | + clang-tidy -p ./build/Release/ --config-file=.clang-tidy ${{ env.GIT_DIFF_FILTERED }} --export-fixes=linter-result/fixes.yml + + - name: Save PR metadata + run: | + echo ${{ github.event.number }} > linter-result/pr-id.txt + echo ${{ github.event.pull_request.head.repo.full_name }} > linter-result/pr-head-repo.txt + echo ${{ github.event.pull_request.head.ref }} > linter-result/pr-head-ref.txt + + - uses: actions/upload-artifact@v2 + with: + name: linter-result + path: linter-result/ + + - name: Run clang-tidy-pr-comments action + uses: platisd/clang-tidy-pr-comments@1.4.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + clang_tidy_fixes: linter-result/fixes.yml + request_changes: true + suggestions_per_comment: 30 \ No newline at end of file diff --git a/.github/workflows/requirements-linter.txt b/.github/workflows/requirements-linter.txt new file mode 100644 index 0000000..a2fa4b7 --- /dev/null +++ b/.github/workflows/requirements-linter.txt @@ -0,0 +1,2 @@ +pyyaml +conan>=1.60.2,<2.0.0 \ No newline at end of file From df6afa8f3c4b278dafc59670b90df507181a9c8b Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 11 Oct 2023 17:26:26 +0200 Subject: [PATCH 04/25] Refactor namespaces and code styling consistency The main change is a refactor of the style used for conditional blocks, method formatting, variable declaration, and namespace formatting in several files. This change makes the style consistent and easier to read and maintain. Furthermore, an unnecessary comment related to command-line utility in the CMakeLists.txt has been removed. The changes don't affect the functionality of the code, but should improve readability and ease of development. Made sure all necessary includes are present Optimized initialization of structs `constexpr`, `noexcept`, list initializers etc. Contributes to CURA-10561 --- CMakeLists.txt | 3 - include/dulcificum/command_types.h | 159 +++++++++++------- .../miracle_jtp/mgjtp_command_to_json.h | 17 +- .../miracle_jtp/mgjtp_json_to_command.h | 14 +- .../mgjtp_mappings_json_key_to_str.h | 110 ++++++------ src/command_types.cpp | 43 ++--- src/miracle_jtp/mgjtp_command_to_json.cpp | 94 ++++++----- src/miracle_jtp/mgjtp_json_to_command.cpp | 68 +++++--- test/dulcificum_unit_tester.cpp | 3 +- test/miracle_jsontoolpath_dialect_test.cpp | 25 +-- test/test_data_dir.h | 9 +- 11 files changed, 309 insertions(+), 236 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9f78ec1..e22ccef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,9 +29,6 @@ if (${EXTENSIVE_WARNINGS}) endif () -# --- Setup command-line utility --- is this still needed - - # --- Setup Python bindings --- diff --git a/include/dulcificum/command_types.h b/include/dulcificum/command_types.h index 50dc5d3..683d5ab 100644 --- a/include/dulcificum/command_types.h +++ b/include/dulcificum/command_types.h @@ -1,32 +1,37 @@ -#ifndef DULCIFICUM_COMMAND_TYPES_H_ -#define DULCIFICUM_COMMAND_TYPES_H_ +#ifndef DULCIFICUM_COMMAND_TYPES_H +#define DULCIFICUM_COMMAND_TYPES_H +#include #include -#include #include -#include +#include +#include -namespace dulcificum { +namespace dulcificum +{ // x, y, z, a, b -typedef std::array ParamPoint; +using ParamPoint = std::array; -namespace botcmd { +namespace botcmd +{ -enum class CommandType { +enum class CommandType : std::uint8_t +{ kInvalid, - kMove, // most commands are move commands - kActiveFanEnable, // look at m_fan_state - kActiveFanDuty, // look at m_fan_speed - kSetTemperature, // change temperature for active tool - kChangeTool, // change active tool - kComment, // do nothing, only emit comments - kDelay, // delay - command to wait for event - kWaitForTemperature, // firmware delays until temp reached - kPause, // Command to allow for user defined pause. + kMove, // most commands are move commands + kActiveFanEnable, // look at m_fan_state + kActiveFanDuty, // look at m_fan_speed + kSetTemperature, // change temperature for active tool + kChangeTool, // change active tool + kComment, // do nothing, only emit comments + kDelay, // delay - command to wait for event + kWaitForTemperature, // firmware delays until temp reached + kPause, // Command to allow for user defined pause. }; -enum class Tag { +enum class Tag : std::uint8_t +{ Invalid, Infill, Inset, @@ -40,81 +45,113 @@ enum class Tag { TravelMove }; -typedef size_t ExtruderIndex; +using ExtruderIndex = size_t; -struct Command { +struct Command +{ Command() = delete; - Command(CommandType type) : type(type) {} + constexpr explicit Command(CommandType type) noexcept + : type{ type } + { + } const CommandType type; std::vector tags; }; -typedef std::shared_ptr CommandPtr; -typedef std::vector CommandList; +using CommandPtr = std::shared_ptr; +using CommandList = std::vector; -struct Comment : public Command { - Comment() - : Command(CommandType::kComment) {} +struct Comment : public Command +{ + Comment() noexcept + : Command(CommandType::kComment) + { + } - std::string comment; + std::string comment{}; }; -struct Move : public Command { - Move() - : Command(CommandType::kMove) {} +struct Move : public Command +{ + constexpr Move() noexcept + : Command(CommandType::kMove) + { + } - ParamPoint point = {NAN, NAN, NAN, NAN, NAN}; - double feedrate = NAN; - std::array is_point_relative = {false, false, false, true, - true}; + ParamPoint point{ NAN, NAN, NAN, NAN, NAN }; + double feedrate{ NAN }; + std::array is_point_relative{ false, false, false, true, true }; }; -struct FanDuty : public Command { - FanDuty() : Command(CommandType::kActiveFanDuty) {} +struct FanDuty : public Command +{ + constexpr FanDuty() noexcept + : Command(CommandType::kActiveFanDuty) + { + } - ExtruderIndex index = 0; - float duty = 0.0; + ExtruderIndex index{ 0 }; + double duty{ 0.0 }; }; -struct FanToggle : public Command { - FanToggle() : Command(CommandType::kActiveFanEnable) {} +struct FanToggle : public Command +{ + constexpr FanToggle() noexcept + : Command(CommandType::kActiveFanEnable) + { + } - ExtruderIndex index = 0; - bool is_on = false; + ExtruderIndex index{ 0 }; + bool is_on{ false }; }; +struct SetTemperature : public Command +{ + constexpr SetTemperature() noexcept + : Command(CommandType::kSetTemperature) + { + } -struct SetTemperature : public Command { - SetTemperature() : Command(CommandType::kSetTemperature) {} - - ExtruderIndex index = 0; - double temperature = 0.0; + ExtruderIndex index{ 0 }; + double temperature{ 0.0 }; }; -struct WaitForTemperature : public Command { - WaitForTemperature() : Command(CommandType::kWaitForTemperature) {} +struct WaitForTemperature : public Command +{ + constexpr WaitForTemperature() noexcept + : Command(CommandType::kWaitForTemperature) + { + } - ExtruderIndex index = 0; + ExtruderIndex index{ 0 }; }; -struct ChangeTool : public Command { - ChangeTool() : Command(CommandType::kChangeTool) {} +struct ChangeTool : public Command +{ + constexpr ChangeTool() noexcept + : Command(CommandType::kChangeTool) + { + } - ExtruderIndex index = 0; - ParamPoint position = {NAN, NAN, NAN, NAN, NAN}; + ExtruderIndex index{ 0 }; + ParamPoint position{ NAN, NAN, NAN, NAN, NAN }; }; -struct Delay : public Command { - Delay() : Command(CommandType::kDelay) {} +struct Delay : public Command +{ + constexpr Delay() noexcept + : Command(CommandType::kDelay) + { + } - double seconds = 0.0; + double seconds{ 0.0 }; }; -CommandPtr spawnCommandPtr(CommandType type); +CommandPtr spawnCommandPtr(const CommandType& type) noexcept; -} -} +} // namespace botcmd +} // namespace dulcificum -#endif //DULCIFICUM_COMMAND_TYPES_H_ +#endif // DULCIFICUM_COMMAND_TYPES_H diff --git a/include/dulcificum/miracle_jtp/mgjtp_command_to_json.h b/include/dulcificum/miracle_jtp/mgjtp_command_to_json.h index 3d20076..b45d2fc 100644 --- a/include/dulcificum/miracle_jtp/mgjtp_command_to_json.h +++ b/include/dulcificum/miracle_jtp/mgjtp_command_to_json.h @@ -1,14 +1,15 @@ -#ifndef DULCIFICUM_MIRACLEGRUE_JSONTOOLPATH_H_ -#define DULCIFICUM_MIRACLEGRUE_JSONTOOLPATH_H_ - -#include +#ifndef DULCIFICUM_MIRACLEGRUE_JSONTOOLPATH_H +#define DULCIFICUM_MIRACLEGRUE_JSONTOOLPATH_H #include "dulcificum/command_types.h" -namespace dulcificum::miracle_jtp { +#include + +namespace dulcificum::miracle_jtp +{ -nlohmann::json toJson(const botcmd::Command &cmd); +nlohmann::json toJson(const botcmd::Command& cmd); -} +} // namespace dulcificum::miracle_jtp -#endif //DULCIFICUM_MIRACLEGRUE_JSONTOOLPATH_H_ +#endif // DULCIFICUM_MIRACLEGRUE_JSONTOOLPATH_H diff --git a/include/dulcificum/miracle_jtp/mgjtp_json_to_command.h b/include/dulcificum/miracle_jtp/mgjtp_json_to_command.h index f14d366..bdcb300 100644 --- a/include/dulcificum/miracle_jtp/mgjtp_json_to_command.h +++ b/include/dulcificum/miracle_jtp/mgjtp_json_to_command.h @@ -1,12 +1,16 @@ #ifndef DULCIFICUM_MIRACLE_GRUE_JSONTOOLPATH_IN_H #define DULCIFICUM_MIRACLE_GRUE_JSONTOOLPATH_IN_H -#include -namespace dulcificum::miracle_jtp { +#include "dulcificum/command_types.h" -botcmd::CommandPtr toCommand(const nlohmann::json &jcmd); +#include -} +namespace dulcificum::miracle_jtp +{ -#endif //DULCIFICUM_MIRACLE_GRUE_JSONTOOLPATH_IN_H +botcmd::CommandPtr toCommand(const nlohmann::json& jcmd); + +} // namespace dulcificum::miracle_jtp + +#endif // DULCIFICUM_MIRACLE_GRUE_JSONTOOLPATH_IN_H diff --git a/include/dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h b/include/dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h index 85d9340..f88d5d0 100644 --- a/include/dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h +++ b/include/dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h @@ -1,68 +1,70 @@ #ifndef DULCIFICUM_MIRACLE_GRUE_JSONTOOLPATH_JSON_KEYS_H #define DULCIFICUM_MIRACLE_GRUE_JSONTOOLPATH_JSON_KEYS_H -#include "dulcificum/miracle_jtp/mgjtp_command_to_json.h" +#include "dulcificum/command_types.h" -namespace dulcificum { +#include +#include -namespace botcmd { +namespace dulcificum +{ -NLOHMANN_JSON_SERIALIZE_ENUM(CommandType, { - { CommandType::kInvalid, "invalid" }, - { CommandType::kMove, "move" }, - { CommandType::kActiveFanEnable, "toggle_fan" }, - { CommandType::kActiveFanDuty, "fan_duty" }, - { CommandType::kSetTemperature, "set_toolhead_temperature" }, - { CommandType::kChangeTool, "change_toolhead" }, - { CommandType::kComment, "comment" }, - { CommandType::kDelay, "delay" }, - { CommandType::kWaitForTemperature, "wait_for_temperature" }, - { CommandType::kPause, "pause" } -}) +namespace botcmd +{ -NLOHMANN_JSON_SERIALIZE_ENUM(Tag, { - { Tag::Invalid, "Invalid" }, - { Tag::Infill, "Infill" }, - { Tag::Inset, "Inset" }, - { Tag::Purge, "Purge" }, - { Tag::QuickToggle, "Quick Toggle" }, - { Tag::Raft, "Raft" }, - { Tag::Restart, "Restart" }, - { Tag::Roof, "Roof" }, - { Tag::Sparse, "Sparse" }, - { Tag::Support, "Support" }, - { Tag::TravelMove, "Travel Move" } -}) +NLOHMANN_JSON_SERIALIZE_ENUM( + CommandType, + { { CommandType::kInvalid, "invalid" }, + { CommandType::kMove, "move" }, + { CommandType::kActiveFanEnable, "toggle_fan" }, + { CommandType::kActiveFanDuty, "fan_duty" }, + { CommandType::kSetTemperature, "set_toolhead_temperature" }, + { CommandType::kChangeTool, "change_toolhead" }, + { CommandType::kComment, "comment" }, + { CommandType::kDelay, "delay" }, + { CommandType::kWaitForTemperature, "wait_for_temperature" }, + { CommandType::kPause, "pause" } }) -} +NLOHMANN_JSON_SERIALIZE_ENUM( + Tag, + { { Tag::Invalid, "Invalid" }, + { Tag::Infill, "Infill" }, + { Tag::Inset, "Inset" }, + { Tag::Purge, "Purge" }, + { Tag::QuickToggle, "Quick Toggle" }, + { Tag::Raft, "Raft" }, + { Tag::Restart, "Restart" }, + { Tag::Roof, "Roof" }, + { Tag::Sparse, "Sparse" }, + { Tag::Support, "Support" }, + { Tag::TravelMove, "Travel Move" } }) -namespace miracle_jtp { +} // namespace botcmd -namespace kKeyStr { +namespace miracle_jtp::kKeyStr +{ -const static std::string a = "a"; -const static std::string b = "b"; -const static std::string command = "command"; -const static std::string comment = "comment"; -const static std::string feedrate = "feedrate"; -const static std::string function = "function"; -const static std::string index = "index"; -const static std::string metadata = "metadata"; -const static std::string parameters = "parameters"; -const static std::string seconds = "seconds"; -const static std::string tags = "tags"; -const static std::string temperature = "temperature"; -const static std::string value = "value"; -const static std::string x = "x"; -const static std::string y = "y"; -const static std::string z = "z"; -const static std::array kParamPointNames{ - x, y, z, a, b -}; +constexpr static std::string_view a{ "a" }; +constexpr static std::string_view b{ "b" }; +constexpr static std::string_view command{ "command" }; +constexpr static std::string_view comment{ "comment" }; +constexpr static std::string_view feedrate{ "feedrate" }; +constexpr static std::string_view function{ "function" }; +constexpr static std::string_view index{ "index" }; +constexpr static std::string_view metadata{ "metadata" }; +constexpr static std::string_view parameters{ "parameters" }; +constexpr static std::string_view seconds{ "seconds" }; +constexpr static std::string_view tags{ "tags" }; +constexpr static std::string_view temperature{ "temperature" }; +constexpr static std::string_view value{ "value" }; +constexpr static std::string_view x{ "x" }; +constexpr static std::string_view y{ "y" }; +constexpr static std::string_view z{ "z" }; +constexpr static std::array kParamPointNames{ x, y, z, a, b }; -} +} // namespace miracle_jtp::kKeyStr -} -} -#endif //DULCIFICUM_MIRACLE_GRUE_JSONTOOLPATH_JSON_KEYS_H +} // namespace dulcificum + +#endif // DULCIFICUM_MIRACLE_GRUE_JSONTOOLPATH_JSON_KEYS_H diff --git a/src/command_types.cpp b/src/command_types.cpp index 124f4d7..bfce167 100644 --- a/src/command_types.cpp +++ b/src/command_types.cpp @@ -1,24 +1,27 @@ #include "dulcificum/command_types.h" -namespace dulcificum::botcmd { -CommandPtr spawnCommandPtr(CommandType type) { - switch (type) { - case CommandType::kMove: - return std::make_shared(); - case CommandType::kActiveFanEnable: - return std::make_shared(); - case CommandType::kActiveFanDuty: - return std::make_shared(); - case CommandType::kDelay: - return std::make_shared(); - case CommandType::kChangeTool: - return std::make_shared(); - case CommandType::kSetTemperature: - return std::make_shared(); - case CommandType::kWaitForTemperature: - return std::make_shared(); - default: - return std::make_shared(type); +namespace dulcificum::botcmd +{ +CommandPtr spawnCommandPtr(const CommandType& type) noexcept +{ + switch (type) + { + case CommandType::kMove: + return std::make_shared(); + case CommandType::kActiveFanEnable: + return std::make_shared(); + case CommandType::kActiveFanDuty: + return std::make_shared(); + case CommandType::kDelay: + return std::make_shared(); + case CommandType::kChangeTool: + return std::make_shared(); + case CommandType::kSetTemperature: + return std::make_shared(); + case CommandType::kWaitForTemperature: + return std::make_shared(); + default: + return std::make_shared(type); } } -} +} // namespace dulcificum::botcmd diff --git a/src/miracle_jtp/mgjtp_command_to_json.cpp b/src/miracle_jtp/mgjtp_command_to_json.cpp index a9cb6dd..784a64a 100644 --- a/src/miracle_jtp/mgjtp_command_to_json.cpp +++ b/src/miracle_jtp/mgjtp_command_to_json.cpp @@ -1,26 +1,31 @@ #include "dulcificum/miracle_jtp/mgjtp_command_to_json.h" + #include "dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h" -namespace dulcificum::miracle_jtp { +namespace dulcificum::miracle_jtp +{ using namespace botcmd; template -nlohmann::json -zipListsToJson(const KeyListType &keys, const ValListType &vals) { +nlohmann::json zipListsToJson(const KeyListType& keys, const ValListType& vals) +{ nlohmann::json jout; - for (size_t key_ii = 0; key_ii < keys.size(); key_ii++) { + for (size_t key_ii = 0; key_ii < keys.size(); key_ii++) + { jout[keys[key_ii]] = vals[key_ii]; } return jout; } template -nlohmann::json -zipListsIgnoreNan(const KeyListType &keys, const ValListType &vals) { +nlohmann::json zipListsIgnoreNan(const KeyListType& keys, const ValListType& vals) +{ nlohmann::json jout; - for (size_t key_ii = 0; key_ii < keys.size(); key_ii++) { - const auto &val = vals[key_ii]; - if (!std::isnan(val)) { + for (size_t key_ii = 0; key_ii < keys.size(); key_ii++) + { + const auto& val = vals[key_ii]; + if (! std::isnan(val)) + { jout[keys[key_ii]] = vals[key_ii]; } } @@ -28,76 +33,79 @@ zipListsIgnoreNan(const KeyListType &keys, const ValListType &vals) { } -nlohmann::json getCommandMetadata(const Command &cmd) { +nlohmann::json getCommandMetadata(const Command& cmd) +{ nlohmann::json jmetadata({}); - if (cmd.type == CommandType::kMove) { - auto move = static_cast(cmd); - jmetadata["relative"] = zipListsToJson( - kKeyStr::kParamPointNames, - move.is_point_relative - ); + if (cmd.type == CommandType::kMove) + { + auto move = static_cast(cmd); + jmetadata["relative"] = zipListsToJson(kKeyStr::kParamPointNames, move.is_point_relative); return jmetadata; } return jmetadata; } -nlohmann::json getCommandParameters(const Command &cmd) { +nlohmann::json getCommandParameters(const Command& cmd) +{ nlohmann::json jparams({}); - if (cmd.type == CommandType::kMove) { - const auto move = static_cast(cmd); - jparams = zipListsToJson( - kKeyStr::kParamPointNames, - move.point - ); + if (cmd.type == CommandType::kMove) + { + const auto move = static_cast(cmd); + jparams = zipListsToJson(kKeyStr::kParamPointNames, move.point); jparams[kKeyStr::feedrate] = move.feedrate; return jparams; } - if (cmd.type == CommandType::kComment) { - const auto com = static_cast(cmd); + if (cmd.type == CommandType::kComment) + { + const auto com = static_cast(cmd); jparams[kKeyStr::comment] = com.comment; return jparams; } - if (cmd.type == CommandType::kActiveFanDuty) { - const auto dut = static_cast(cmd); + if (cmd.type == CommandType::kActiveFanDuty) + { + const auto dut = static_cast(cmd); jparams[kKeyStr::index] = dut.index; jparams[kKeyStr::value] = dut.duty; return jparams; } - if (cmd.type == CommandType::kActiveFanEnable) { - const auto fan = static_cast(cmd); + if (cmd.type == CommandType::kActiveFanEnable) + { + const auto fan = static_cast(cmd); jparams[kKeyStr::index] = fan.index; jparams[kKeyStr::value] = fan.is_on; return jparams; } - if (cmd.type == CommandType::kSetTemperature) { - const auto dcmd = static_cast(cmd); + if (cmd.type == CommandType::kSetTemperature) + { + const auto dcmd = static_cast(cmd); jparams[kKeyStr::index] = dcmd.index; jparams[kKeyStr::temperature] = dcmd.temperature; return jparams; } - if (cmd.type == CommandType::kWaitForTemperature) { - const auto dcmd = static_cast(cmd); + if (cmd.type == CommandType::kWaitForTemperature) + { + const auto dcmd = static_cast(cmd); jparams[kKeyStr::index] = dcmd.index; return jparams; } - if (cmd.type == CommandType::kChangeTool) { - const auto dcmd = static_cast(cmd); - jparams = zipListsIgnoreNan( - kKeyStr::kParamPointNames, - dcmd.position - ); + if (cmd.type == CommandType::kChangeTool) + { + const auto dcmd = static_cast(cmd); + jparams = zipListsIgnoreNan(kKeyStr::kParamPointNames, dcmd.position); jparams[kKeyStr::index] = dcmd.index; return jparams; } - if (cmd.type == CommandType::kDelay) { - const auto dcmd = static_cast(cmd); + if (cmd.type == CommandType::kDelay) + { + const auto dcmd = static_cast(cmd); jparams[kKeyStr::seconds] = dcmd.seconds; return jparams; } return jparams; } -nlohmann::json toJson(const Command &cmd) { +nlohmann::json toJson(const Command& cmd) +{ nlohmann::json jcmd; jcmd[kKeyStr::function] = cmd.type; jcmd[kKeyStr::metadata] = getCommandMetadata(cmd); @@ -107,4 +115,4 @@ nlohmann::json toJson(const Command &cmd) { jout[kKeyStr::command] = jcmd; return jout; } -} \ No newline at end of file +} // namespace dulcificum::miracle_jtp \ No newline at end of file diff --git a/src/miracle_jtp/mgjtp_json_to_command.cpp b/src/miracle_jtp/mgjtp_json_to_command.cpp index 98531c5..d09a70b 100644 --- a/src/miracle_jtp/mgjtp_json_to_command.cpp +++ b/src/miracle_jtp/mgjtp_json_to_command.cpp @@ -1,17 +1,20 @@ #include "dulcificum/miracle_jtp/mgjtp_command_to_json.h" +#include "dulcificum/command_types.h" #include "dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h" -namespace dulcificum::miracle_jtp { +namespace dulcificum::miracle_jtp +{ using namespace botcmd; -std::shared_ptr toMove(const nlohmann::json &jmove) { +std::shared_ptr toMove(const nlohmann::json& jmove) +{ auto move = std::make_shared(); - const auto &jparams = jmove.at(kKeyStr::parameters); + const auto& jparams = jmove.at(kKeyStr::parameters); move->feedrate = jparams.at(kKeyStr::feedrate); - const auto &jrelative = jmove.at(kKeyStr::metadata).at("relative"); - for (size_t param_ii = 0; - param_ii < kKeyStr::kParamPointNames.size(); param_ii++) { - const auto &key = kKeyStr::kParamPointNames[param_ii]; + const auto& jrelative = jmove.at(kKeyStr::metadata).at("relative"); + for (size_t param_ii = 0; param_ii < kKeyStr::kParamPointNames.size(); param_ii++) + { + const auto& key = kKeyStr::kParamPointNames[param_ii]; double pval = jparams.at(key); move->point[param_ii] = pval; bool is_rel = jrelative.at(key); @@ -22,43 +25,50 @@ std::shared_ptr toMove(const nlohmann::json &jmove) { return move; } -CommandPtr -toParamOnlyCommand(const CommandType type, const nlohmann::json &jparam) { - if (type == CommandType::kComment) { +CommandPtr toParamOnlyCommand(const CommandType type, const nlohmann::json& jparam) +{ + if (type == CommandType::kComment) + { auto com = std::make_shared(); com->comment = jparam.at(kKeyStr::comment); return com; } - if (type == CommandType::kActiveFanDuty) { + if (type == CommandType::kActiveFanDuty) + { auto cmd = std::make_shared(); cmd->index = jparam[kKeyStr::index]; cmd->duty = jparam[kKeyStr::value]; return cmd; } - if (type == CommandType::kActiveFanEnable) { + if (type == CommandType::kActiveFanEnable) + { auto cmd = std::make_shared(); cmd->index = jparam[kKeyStr::index]; cmd->is_on = jparam[kKeyStr::value]; return cmd; } - if (type == CommandType::kSetTemperature) { + if (type == CommandType::kSetTemperature) + { auto cmd = std::make_shared(); cmd->index = jparam[kKeyStr::index]; cmd->temperature = jparam[kKeyStr::temperature]; return cmd; } - if (type == CommandType::kWaitForTemperature) { + if (type == CommandType::kWaitForTemperature) + { auto cmd = std::make_shared(); cmd->index = jparam[kKeyStr::index]; return cmd; } - if (type == CommandType::kChangeTool) { + if (type == CommandType::kChangeTool) + { auto cmd = std::make_shared(); cmd->index = jparam[kKeyStr::index]; cmd->temperature = jparam[kKeyStr::temperature]; return cmd; } - if (type == CommandType::kDelay) { + if (type == CommandType::kDelay) + { auto cmd = std::make_shared(); cmd->seconds = jparam[kKeyStr::seconds]; return cmd; @@ -66,14 +76,16 @@ toParamOnlyCommand(const CommandType type, const nlohmann::json &jparam) { return spawnCommandPtr(type); } -CommandPtr toChangeTool(const nlohmann::json &jcmd) { +CommandPtr toChangeTool(const nlohmann::json& jcmd) +{ auto cmd = std::make_shared(); auto jparam = jcmd.at(kKeyStr::parameters); cmd->index = jparam.at(kKeyStr::index); - for (size_t param_ii = 0; - param_ii < kKeyStr::kParamPointNames.size(); param_ii++) { - const auto &key = kKeyStr::kParamPointNames[param_ii]; - if (jparam.contains(key)) { + for (size_t param_ii = 0; param_ii < kKeyStr::kParamPointNames.size(); param_ii++) + { + const auto& key = kKeyStr::kParamPointNames[param_ii]; + if (jparam.contains(key)) + { cmd->position[param_ii] = jparam.at(key); } } @@ -82,16 +94,20 @@ CommandPtr toChangeTool(const nlohmann::json &jcmd) { return cmd; } -CommandPtr toCommand(const nlohmann::json &jin) { +CommandPtr toCommand(const nlohmann::json& jin) +{ auto jcmd = jin[kKeyStr::command]; CommandType type = jcmd[kKeyStr::function]; - if (type == CommandType::kMove) { + if (type == CommandType::kMove) + { return toMove(jcmd); } - if (type == CommandType::kChangeTool) { + if (type == CommandType::kChangeTool) + { return toChangeTool(jcmd); } - if (jcmd.contains(kKeyStr::parameters)) { + if (jcmd.contains(kKeyStr::parameters)) + { auto jparam = jcmd[kKeyStr::parameters]; auto cmd = toParamOnlyCommand(type, jparam); std::vector tags = jcmd.at(kKeyStr::tags); @@ -102,4 +118,4 @@ CommandPtr toCommand(const nlohmann::json &jin) { return cmd; } -} +} // namespace dulcificum::miracle_jtp diff --git a/test/dulcificum_unit_tester.cpp b/test/dulcificum_unit_tester.cpp index 4a89c5e..e74059b 100644 --- a/test/dulcificum_unit_tester.cpp +++ b/test/dulcificum_unit_tester.cpp @@ -1,6 +1,7 @@ #include "gtest/gtest.h" -int main(int argc, char **argv) { +int main(int argc, char** argv) +{ testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } \ No newline at end of file diff --git a/test/miracle_jsontoolpath_dialect_test.cpp b/test/miracle_jsontoolpath_dialect_test.cpp index eeec991..82b7778 100644 --- a/test/miracle_jsontoolpath_dialect_test.cpp +++ b/test/miracle_jsontoolpath_dialect_test.cpp @@ -1,41 +1,44 @@ -#include "gtest/gtest.h" - -#include "dulcificum/miracle_jtp/mgjtp_json_to_command.h" #include "dulcificum/miracle_jtp/mgjtp_command_to_json.h" +#include "dulcificum/miracle_jtp/mgjtp_json_to_command.h" +#include "test_data_dir.h" +#include "gtest/gtest.h" #include -#include "test_data_dir.h" using namespace dulcificum; -TEST(miracle_jtp_tests, move) { +TEST(miracle_jtp_tests, move) +{ botcmd::Move move; move.feedrate = 0.4242; - move.point = {-4.2, 42.0, 0.42, 0.0, 0.12}; - move.tags = {botcmd::Tag::Roof}; + move.point = { -4.2, 42.0, 0.42, 0.0, 0.12 }; + move.tags = { botcmd::Tag::Roof }; auto jmove = miracle_jtp::toJson(move); auto cmove = miracle_jtp::toCommand(jmove); auto jmove1 = miracle_jtp::toJson(*cmove); EXPECT_EQ(jmove, jmove1); } -TEST(miracle_jtp_tests, change_toolhead) { +TEST(miracle_jtp_tests, change_toolhead) +{ botcmd::ChangeTool cmd; cmd.position[0] = -4.2; cmd.position[1] = 42.0; - cmd.tags = {botcmd::Tag::TravelMove}; + cmd.tags = { botcmd::Tag::TravelMove }; auto jcmd = miracle_jtp::toJson(cmd); auto cmd1 = miracle_jtp::toCommand(jcmd); auto jcmd1 = miracle_jtp::toJson(*cmd1); EXPECT_EQ(jcmd, jcmd1); } -TEST(miracle_jtp_tests, rwrw) { +TEST(miracle_jtp_tests, rwrw) +{ std::filesystem::path example_path = kTestDataDir / "cmd_example.json"; ASSERT_TRUE(std::filesystem::exists(example_path)); std::ifstream fin(example_path); nlohmann::json jin = nlohmann::json::parse(fin); - for (const auto& jcmd : jin) { + for (const auto& jcmd : jin) + { const auto cmd0 = miracle_jtp::toCommand(jcmd); const auto jcmd1 = miracle_jtp::toJson(*cmd0); EXPECT_EQ(jcmd, jcmd1); diff --git a/test/test_data_dir.h b/test/test_data_dir.h index 334b9d2..c1b308d 100644 --- a/test/test_data_dir.h +++ b/test/test_data_dir.h @@ -4,8 +4,9 @@ #include #include -namespace dulcificum { - static const auto kTestDataDir = std::filesystem::path { std::source_location::current().file_name() }.parent_path().append("data"); -} +namespace dulcificum +{ +static const auto kTestDataDir = std::filesystem::path{ std::source_location::current().file_name() }.parent_path().append("data"); +} // namespace dulcificum -#endif //DULCIFICUM_TEST_DATA_DIR_H +#endif // DULCIFICUM_TEST_DATA_DIR_H From a761b3d0da03cfa930f3cf4652f942dcf11e3f4a Mon Sep 17 00:00:00 2001 From: jellespijker Date: Wed, 11 Oct 2023 15:27:03 +0000 Subject: [PATCH 05/25] Applied clang-format. --- src/miracle_jtp/mgjtp_json_to_command.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/miracle_jtp/mgjtp_json_to_command.cpp b/src/miracle_jtp/mgjtp_json_to_command.cpp index d09a70b..8abaae4 100644 --- a/src/miracle_jtp/mgjtp_json_to_command.cpp +++ b/src/miracle_jtp/mgjtp_json_to_command.cpp @@ -1,5 +1,5 @@ -#include "dulcificum/miracle_jtp/mgjtp_command_to_json.h" #include "dulcificum/command_types.h" +#include "dulcificum/miracle_jtp/mgjtp_command_to_json.h" #include "dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h" namespace dulcificum::miracle_jtp From ab138dc50b213cc8748cf57ee7a88c4d0bcc53b8 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 11 Oct 2023 17:34:43 +0200 Subject: [PATCH 06/25] Remove 'class' keyword from enums MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'class' keyword was eliminated from CommandType and Tag enumerations in 'command_types.h'. Fixes warning: elaborated-type-specifier for a scoped enum must not use the ‘class’ keyword Contributes to CURA-10561 --- include/dulcificum/command_types.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/dulcificum/command_types.h b/include/dulcificum/command_types.h index 683d5ab..fdd7b76 100644 --- a/include/dulcificum/command_types.h +++ b/include/dulcificum/command_types.h @@ -16,7 +16,7 @@ using ParamPoint = std::array; namespace botcmd { -enum class CommandType : std::uint8_t +enum CommandType : std::uint8_t { kInvalid, kMove, // most commands are move commands @@ -30,7 +30,7 @@ enum class CommandType : std::uint8_t kPause, // Command to allow for user defined pause. }; -enum class Tag : std::uint8_t +enum Tag : std::uint8_t { Invalid, Infill, From 72d6a80594040df84561a2c18909cc4561be9f08 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 11 Oct 2023 17:48:09 +0200 Subject: [PATCH 07/25] Replace command_types.h with mgjtp_command_to_json.h The include directives changed in several files: dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h, src/miracle_jtp/mgjtp_json_to_command.cpp, src/miracle_jtp/mgjtp_command_to_json.cpp, and include/dulcificum/miracle_jtp/mgjtp_json_to_command.h. All references to "dulcificum/command_types.h" have now been replaced with "dulcificum/miracle_jtp/mgjtp_command_to_json.h". This change was necessary as mgjtp_command_to_json.h provides more relevant functionality. Contributes to CURA-10561 --- include/dulcificum/miracle_jtp/mgjtp_json_to_command.h | 3 +-- .../dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h | 2 +- src/miracle_jtp/mgjtp_command_to_json.cpp | 1 - src/miracle_jtp/mgjtp_json_to_command.cpp | 1 - 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/include/dulcificum/miracle_jtp/mgjtp_json_to_command.h b/include/dulcificum/miracle_jtp/mgjtp_json_to_command.h index bdcb300..954bf77 100644 --- a/include/dulcificum/miracle_jtp/mgjtp_json_to_command.h +++ b/include/dulcificum/miracle_jtp/mgjtp_json_to_command.h @@ -1,8 +1,7 @@ #ifndef DULCIFICUM_MIRACLE_GRUE_JSONTOOLPATH_IN_H #define DULCIFICUM_MIRACLE_GRUE_JSONTOOLPATH_IN_H - -#include "dulcificum/command_types.h" +#include "dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h" #include diff --git a/include/dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h b/include/dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h index f88d5d0..62a8afd 100644 --- a/include/dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h +++ b/include/dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h @@ -1,7 +1,7 @@ #ifndef DULCIFICUM_MIRACLE_GRUE_JSONTOOLPATH_JSON_KEYS_H #define DULCIFICUM_MIRACLE_GRUE_JSONTOOLPATH_JSON_KEYS_H -#include "dulcificum/command_types.h" +#include "dulcificum/miracle_jtp/mgjtp_command_to_json.h" #include #include diff --git a/src/miracle_jtp/mgjtp_command_to_json.cpp b/src/miracle_jtp/mgjtp_command_to_json.cpp index 784a64a..34c3124 100644 --- a/src/miracle_jtp/mgjtp_command_to_json.cpp +++ b/src/miracle_jtp/mgjtp_command_to_json.cpp @@ -1,5 +1,4 @@ #include "dulcificum/miracle_jtp/mgjtp_command_to_json.h" - #include "dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h" namespace dulcificum::miracle_jtp diff --git a/src/miracle_jtp/mgjtp_json_to_command.cpp b/src/miracle_jtp/mgjtp_json_to_command.cpp index 8abaae4..d90a8a2 100644 --- a/src/miracle_jtp/mgjtp_json_to_command.cpp +++ b/src/miracle_jtp/mgjtp_json_to_command.cpp @@ -1,4 +1,3 @@ -#include "dulcificum/command_types.h" #include "dulcificum/miracle_jtp/mgjtp_command_to_json.h" #include "dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h" From d54acd059e3cce33003d8a0ae641a1e1dae57319 Mon Sep 17 00:00:00 2001 From: jellespijker Date: Wed, 11 Oct 2023 15:48:46 +0000 Subject: [PATCH 08/25] Applied clang-format. --- src/miracle_jtp/mgjtp_command_to_json.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/miracle_jtp/mgjtp_command_to_json.cpp b/src/miracle_jtp/mgjtp_command_to_json.cpp index 34c3124..784a64a 100644 --- a/src/miracle_jtp/mgjtp_command_to_json.cpp +++ b/src/miracle_jtp/mgjtp_command_to_json.cpp @@ -1,4 +1,5 @@ #include "dulcificum/miracle_jtp/mgjtp_command_to_json.h" + #include "dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h" namespace dulcificum::miracle_jtp From e7167e7edcb03ea3b0e3c2d984665572d11864ee Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 11 Oct 2023 18:00:44 +0200 Subject: [PATCH 09/25] revert uint8_t enum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Should fix the `error: use of enum ‘CommandType’ without previous declaration` Contributes to CURA-10561 --- include/dulcificum/command_types.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/dulcificum/command_types.h b/include/dulcificum/command_types.h index fdd7b76..0f0f3c4 100644 --- a/include/dulcificum/command_types.h +++ b/include/dulcificum/command_types.h @@ -16,7 +16,7 @@ using ParamPoint = std::array; namespace botcmd { -enum CommandType : std::uint8_t +enum class CommandType { kInvalid, kMove, // most commands are move commands @@ -30,7 +30,7 @@ enum CommandType : std::uint8_t kPause, // Command to allow for user defined pause. }; -enum Tag : std::uint8_t +enum class Tag { Invalid, Infill, From e491845bd222bc2433bb8675bc4ab6666571f0b5 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 11 Oct 2023 18:08:42 +0200 Subject: [PATCH 10/25] Removed `constexpr` Tag isn't constexpr Contributes to CURA-10561 --- include/dulcificum/command_types.h | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/include/dulcificum/command_types.h b/include/dulcificum/command_types.h index 0f0f3c4..d83776d 100644 --- a/include/dulcificum/command_types.h +++ b/include/dulcificum/command_types.h @@ -51,7 +51,7 @@ struct Command { Command() = delete; - constexpr explicit Command(CommandType type) noexcept + explicit Command(CommandType type) noexcept : type{ type } { } @@ -75,7 +75,7 @@ struct Comment : public Command struct Move : public Command { - constexpr Move() noexcept + Move() noexcept : Command(CommandType::kMove) { } @@ -87,7 +87,7 @@ struct Move : public Command struct FanDuty : public Command { - constexpr FanDuty() noexcept + FanDuty() noexcept : Command(CommandType::kActiveFanDuty) { } @@ -98,7 +98,7 @@ struct FanDuty : public Command struct FanToggle : public Command { - constexpr FanToggle() noexcept + FanToggle() noexcept : Command(CommandType::kActiveFanEnable) { } @@ -109,7 +109,7 @@ struct FanToggle : public Command struct SetTemperature : public Command { - constexpr SetTemperature() noexcept + SetTemperature() noexcept : Command(CommandType::kSetTemperature) { } @@ -120,7 +120,7 @@ struct SetTemperature : public Command struct WaitForTemperature : public Command { - constexpr WaitForTemperature() noexcept + WaitForTemperature() noexcept : Command(CommandType::kWaitForTemperature) { } @@ -130,7 +130,7 @@ struct WaitForTemperature : public Command struct ChangeTool : public Command { - constexpr ChangeTool() noexcept + ChangeTool() noexcept : Command(CommandType::kChangeTool) { } @@ -141,7 +141,7 @@ struct ChangeTool : public Command struct Delay : public Command { - constexpr Delay() noexcept + Delay() noexcept : Command(CommandType::kDelay) { } From 4f6d7e78748dbf9b1c42f7a7e9162afc210f7719 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 11 Oct 2023 18:21:06 +0200 Subject: [PATCH 11/25] Revert "Removed `constexpr`" This reverts commit e491845bd222bc2433bb8675bc4ab6666571f0b5. --- include/dulcificum/command_types.h | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/include/dulcificum/command_types.h b/include/dulcificum/command_types.h index d83776d..0f0f3c4 100644 --- a/include/dulcificum/command_types.h +++ b/include/dulcificum/command_types.h @@ -51,7 +51,7 @@ struct Command { Command() = delete; - explicit Command(CommandType type) noexcept + constexpr explicit Command(CommandType type) noexcept : type{ type } { } @@ -75,7 +75,7 @@ struct Comment : public Command struct Move : public Command { - Move() noexcept + constexpr Move() noexcept : Command(CommandType::kMove) { } @@ -87,7 +87,7 @@ struct Move : public Command struct FanDuty : public Command { - FanDuty() noexcept + constexpr FanDuty() noexcept : Command(CommandType::kActiveFanDuty) { } @@ -98,7 +98,7 @@ struct FanDuty : public Command struct FanToggle : public Command { - FanToggle() noexcept + constexpr FanToggle() noexcept : Command(CommandType::kActiveFanEnable) { } @@ -109,7 +109,7 @@ struct FanToggle : public Command struct SetTemperature : public Command { - SetTemperature() noexcept + constexpr SetTemperature() noexcept : Command(CommandType::kSetTemperature) { } @@ -120,7 +120,7 @@ struct SetTemperature : public Command struct WaitForTemperature : public Command { - WaitForTemperature() noexcept + constexpr WaitForTemperature() noexcept : Command(CommandType::kWaitForTemperature) { } @@ -130,7 +130,7 @@ struct WaitForTemperature : public Command struct ChangeTool : public Command { - ChangeTool() noexcept + constexpr ChangeTool() noexcept : Command(CommandType::kChangeTool) { } @@ -141,7 +141,7 @@ struct ChangeTool : public Command struct Delay : public Command { - Delay() noexcept + constexpr Delay() noexcept : Command(CommandType::kDelay) { } From 2b0a4def2d792884dabc076af543d569a47cd1de Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 11 Oct 2023 18:28:20 +0200 Subject: [PATCH 12/25] no test options available we use the conf file Contributes to CURA-10561 --- .github/workflows/lint-tidier.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-tidier.yml b/.github/workflows/lint-tidier.yml index 7fc316c..0d25f64 100644 --- a/.github/workflows/lint-tidier.yml +++ b/.github/workflows/lint-tidier.yml @@ -71,7 +71,7 @@ jobs: conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}" - name: Install dependencies - run: conan install . ${{ needs.conan-recipe-version.outputs.recipe_id_full }} -o enable_testing=True -s build_type=Release --build=missing --update -g GitHubActionsRunEnv -g GitHubActionsBuildEnv + run: conan install . ${{ needs.conan-recipe-version.outputs.recipe_id_full }} -s build_type=Release --build=missing --update -g GitHubActionsRunEnv -g GitHubActionsBuildEnv - name: Set Environment variables from Conan install (bash) if: ${{ runner.os != 'Windows' }} From 24c2f9759efff0eee1589525bfe8e82fffb36c96 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 12 Oct 2023 10:56:48 +0200 Subject: [PATCH 13/25] Add 'translator' application and its build setup The 'translator' application has been added to the project. Relying on several libraries and offering command line options, it serves as primary user interface. The libraries used include 'docopt' for command line parsing, 'spdlog' for logging and 'jinja2' for templating. Build files 'CMakeLists.txt' have been updated accordingly to include applications and tests based on the new options. Besides, '.gitignore' has been updated to ignore generated header file 'cmdline.h'. This update also modifies 'conanfile.py' to include new requirements and options, as well as to generate 'cmdline.h' from a template. This streamlines the application build process, making it more flexible and maintainable. Contribute to CURA-10561 --- .gitignore | 2 ++ CMakeLists.txt | 18 ++++++++++++++---- apps/CMakeLists.txt | 14 ++++++++++++++ apps/translator_main.cpp | 27 +++++++++++++++++++++++++++ conanfile.py | 21 +++++++++++++++++++++ templates/cmdline.h.jinja | 34 ++++++++++++++++++++++++++++++++++ 6 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 apps/CMakeLists.txt create mode 100644 apps/translator_main.cpp create mode 100644 templates/cmdline.h.jinja diff --git a/.gitignore b/.gitignore index 5d24095..4d5fda8 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,5 @@ conanbuildinfo.txt conaninfo.txt graph_info.json +apps/cmdline.h + diff --git a/CMakeLists.txt b/CMakeLists.txt index e22ccef..28efce9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,8 +4,10 @@ project(dulcificum) find_package(standardprojectsettings REQUIRED) option(EXTENSIVE_WARNINGS "Build with all warnings" ON) option(ENABLE_TESTS "Build with unit test" ON) +option(WITH_APPS "Build with apps" ON) find_package(nlohmann_json REQUIRED) +find_package(spdlog REQUIRED) # --- Setup the shared C++ mgjtp library --- set(DULCIFICUM_SRC @@ -14,7 +16,11 @@ set(DULCIFICUM_SRC src/miracle_jtp/mgjtp_json_to_command.cpp ) add_library(dulcificum ${DULCIFICUM_SRC}) -target_link_libraries(dulcificum PUBLIC nlohmann_json::nlohmann_json) +target_link_libraries(dulcificum + PUBLIC + nlohmann_json::nlohmann_json + PRIVATE + spdlog::spdlog) target_include_directories(dulcificum PUBLIC @@ -28,11 +34,15 @@ if (${EXTENSIVE_WARNINGS}) set_project_warnings(dulcificum) endif () - # --- Setup Python bindings --- -# --- Setup tests +# --- Setup tests --- if (ENABLE_TESTS) add_subdirectory(test) -endif () \ No newline at end of file +endif () + +# --- Setup Apps --- +if (WITH_APPS) + add_subdirectory(apps) +endif () diff --git a/apps/CMakeLists.txt b/apps/CMakeLists.txt new file mode 100644 index 0000000..9da4d99 --- /dev/null +++ b/apps/CMakeLists.txt @@ -0,0 +1,14 @@ +find_package(docopt REQUIRED) + + +add_executable(translator + translator_main.cpp +) + +enable_sanitizers(translator) +if (${EXTENSIVE_WARNINGS}) + set_project_warnings(translator) +endif () + +target_link_libraries(translator PUBLIC dulcificum docopt_s spdlog::spdlog) + diff --git a/apps/translator_main.cpp b/apps/translator_main.cpp new file mode 100644 index 0000000..d6177e6 --- /dev/null +++ b/apps/translator_main.cpp @@ -0,0 +1,27 @@ +#include "cmdline.h" + +#include + +#include +#include + +int main(int argc, const char** argv) +{ + constexpr bool show_help = true; + const std::map args + = docopt::docopt(fmt::format(apps::cmdline::USAGE, apps::cmdline::NAME), { argv + 1, argv + argc }, show_help, apps::cmdline::VERSION_ID); + + if (args.contains("--quiet")) + { + spdlog::set_level(spdlog::level::err); + } + else if (args.contains("--verbose")) + { + spdlog::set_level(spdlog::level::debug); + } + else + { + spdlog::set_level(spdlog::level::info); + } + spdlog::info("Tasting the menu"); +} \ No newline at end of file diff --git a/conanfile.py b/conanfile.py index 93b3074..63951c4 100644 --- a/conanfile.py +++ b/conanfile.py @@ -8,6 +8,7 @@ from conan.tools.files import copy, mkdir, AutoPackager from conan.tools.microsoft import check_min_vs, is_msvc_static_runtime, is_msvc from conan.tools.scm import Version +from jinja2 import Template required_conan_version = ">=1.58.0 <2.0.0" @@ -27,11 +28,13 @@ class DulcificumConan(ConanFile): "shared": [True, False], "fPIC": [True, False], "enable_extensive_warnings": [True, False], + "with_apps": [True, False], } default_options = { "shared": False, "fPIC": True, "enable_extensive_warnings": False, + "with_apps": True, } def set_version(self): @@ -52,11 +55,23 @@ def _compilers_minimum_version(self): "visual_studio": "17", } + def _generate_cmdline(self): + with open(os.path.join(self.source_folder, "templates", "cmdline.h.jinja"), "r") as f: + template = Template(f.read()) + + version = Version(self.version) + with open(os.path.join(self.source_folder, "apps", "cmdline.h"), "w") as f: + f.write(template.render(app_name = "translator", + description = self.description, + version = f"{version.major}.{version.minor}.{version.patch}")) + def export_sources(self): copy(self, "CMakeLists.txt", self.recipe_folder, self.export_sources_folder) copy(self, "*", os.path.join(self.recipe_folder, "src"), os.path.join(self.export_sources_folder, "src")) copy(self, "*", os.path.join(self.recipe_folder, "include"), os.path.join(self.export_sources_folder, "include")) copy(self, "*", os.path.join(self.recipe_folder, "test"), os.path.join(self.export_sources_folder, "test")) + copy(self, "*", os.path.join(self.recipe_folder, "apps"), os.path.join(self.export_sources_folder, "apps")) + copy(self, "*", os.path.join(self.recipe_folder, "templates"), os.path.join(self.export_sources_folder, "templates")) def config_options(self): if self.settings.os == "Windows": @@ -72,6 +87,9 @@ def layout(self): def requirements(self): self.requires("nlohmann_json/3.11.2", transitive_headers = True) + self.requires("spdlog/1.10.0") + if self.options.with_apps: + self.requires("docopt.cpp/0.6.3") def build_requirements(self): self.test_requires("standardprojectsettings/[>=0.1.0]@ultimaker/stable") @@ -92,9 +110,12 @@ def validate(self): raise ConanInvalidConfiguration(f"{self.ref} can not be built as shared on Visual Studio and msvc.") def generate(self): + self._generate_cmdline() + tc = CMakeToolchain(self) tc.variables["ENABLE_TESTS"] = not self.conf.get("tools.build:skip_test", False, check_type = bool) tc.variables["EXTENSIVE_WARNINGS"] = self.options.enable_extensive_warnings + tc.variables["WITH_APPS"] = self.options.with_apps if is_msvc(self): tc.variables["USE_MSVC_RUNTIME_LIBRARY_DLL"] = not is_msvc_static_runtime(self) tc.cache_variables["CMAKE_POLICY_DEFAULT_CMP0077"] = "NEW" diff --git a/templates/cmdline.h.jinja b/templates/cmdline.h.jinja new file mode 100644 index 0000000..e14d4c4 --- /dev/null +++ b/templates/cmdline.h.jinja @@ -0,0 +1,34 @@ +#ifndef APPS_CMDLINE_H +#define APPS_CMDLINE_H + +#include + +#include + +namespace apps::cmdline +{ + +constexpr std::string_view NAME = "{{ app_name }}"; +constexpr std::string_view VERSION = "{{ version }}"; +static const auto VERSION_ID = fmt::format(FMT_COMPILE("{} {}"), NAME, VERSION); + +constexpr std::string_view USAGE = R"({0}. +{{ description }} + +Usage: + {{ app_name }} [--quiet | --verbose] --input=FLAVOR INPUT --output=FLAVOR OUPUT + {{ app_name }} (-h | --help) + {{ app_name }} --version + +Options: + -h --help Show this screen. + --version Show version. + --quiet Output no text to stdout (except errors) + --verbose Output more log statements + --input=TYPE 'miracle_jtp' | 'griffin' + --output=FLAVOR 'miracle_jtp' | 'griffin' +)"; + +} // namespace apps::cmdline + +#endif // APPS_CMDLINE_H \ No newline at end of file From 3f593af97f892b6913a95502bbe5392d8dec7049 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 12 Oct 2023 12:39:30 +0200 Subject: [PATCH 14/25] Added Python bindings for the Dulcificum bot library Python binding for Dulcificum bot library has been initialized. This includes necessary includes and setup in `pyDulcificum.cpp` file, modification in `CMakeLists.txt` for option to build with Python bindings, and updating the Conan file to include Python bindings option and requirements necessary for these bindings. This will allow Python scripts to directly use Dulcificum's bot commands and functionalities. Contributes to CURA-10561 --- CMakeLists.txt | 12 +++++++++++- conanfile.py | 18 ++++++++++++++++++ pyDulcificum/pyDulcificum.cpp | 25 +++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 pyDulcificum/pyDulcificum.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 28efce9..0c0dd78 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,7 @@ find_package(standardprojectsettings REQUIRED) option(EXTENSIVE_WARNINGS "Build with all warnings" ON) option(ENABLE_TESTS "Build with unit test" ON) option(WITH_APPS "Build with apps" ON) +option(WITH_PYTHON_BINDINGS "Build with Python bindings: `pyDulcificum`" ON) find_package(nlohmann_json REQUIRED) find_package(spdlog REQUIRED) @@ -35,7 +36,16 @@ if (${EXTENSIVE_WARNINGS}) endif () # --- Setup Python bindings --- - +if (WITH_PYTHON_BINDINGS) + find_package(pybind11 REQUIRED) + find_package(cpython REQUIRED) + + pybind11_add_module(pyDulcificum pyDulcificum/pyDulcificum.cpp) + target_link_libraries(pyDulcificum PUBLIC dulcificum pybind11::pybind11 cpython::cpython) + if(NOT MSVC AND NOT ${CMAKE_BUILD_TYPE} MATCHES Debug|RelWithDebInfo) + pybind11_strip(pyArcus) + endif() +endif () # --- Setup tests --- if (ENABLE_TESTS) diff --git a/conanfile.py b/conanfile.py index 63951c4..d1c1890 100644 --- a/conanfile.py +++ b/conanfile.py @@ -29,12 +29,14 @@ class DulcificumConan(ConanFile): "fPIC": [True, False], "enable_extensive_warnings": [True, False], "with_apps": [True, False], + "with_python_bindings": [True, False], } default_options = { "shared": False, "fPIC": True, "enable_extensive_warnings": False, "with_apps": True, + "with_python_bindings": True, } def set_version(self): @@ -71,6 +73,7 @@ def export_sources(self): copy(self, "*", os.path.join(self.recipe_folder, "include"), os.path.join(self.export_sources_folder, "include")) copy(self, "*", os.path.join(self.recipe_folder, "test"), os.path.join(self.export_sources_folder, "test")) copy(self, "*", os.path.join(self.recipe_folder, "apps"), os.path.join(self.export_sources_folder, "apps")) + copy(self, "*", os.path.join(self.recipe_folder, "pyDulcificum"), os.path.join(self.export_sources_folder, "pyDulcificum")) copy(self, "*", os.path.join(self.recipe_folder, "templates"), os.path.join(self.export_sources_folder, "templates")) def config_options(self): @@ -90,6 +93,9 @@ def requirements(self): self.requires("spdlog/1.10.0") if self.options.with_apps: self.requires("docopt.cpp/0.6.3") + if self.options.with_python_bindings: + self.requires("cpython/3.10.4") + self.requires("pybind11/2.10.4") def build_requirements(self): self.test_requires("standardprojectsettings/[>=0.1.0]@ultimaker/stable") @@ -115,7 +121,19 @@ def generate(self): tc = CMakeToolchain(self) tc.variables["ENABLE_TESTS"] = not self.conf.get("tools.build:skip_test", False, check_type = bool) tc.variables["EXTENSIVE_WARNINGS"] = self.options.enable_extensive_warnings + tc.variables["WITH_APPS"] = self.options.with_apps + + tc.variables["WITH_PYTHON_BINDINGS"] = self.options.with_python_bindings + if self.options.with_python_bindings: + tc.variables["Python_EXECUTABLE"] = self.deps_user_info["cpython"].python.replace("\\", "/") + tc.variables["Python_USE_STATIC_LIBS"] = not self.options["cpython"].shared + tc.variables["Python_ROOT_DIR"] = self.deps_cpp_info["cpython"].rootpath.replace("\\", "/") + tc.variables["Python_FIND_FRAMEWORK"] = "NEVER" + tc.variables["Python_FIND_REGISTRY"] = "NEVER" + tc.variables["Python_FIND_IMPLEMENTATIONS"] = "CPython" + tc.variables["Python_FIND_STRATEGY"] = "LOCATION" + if is_msvc(self): tc.variables["USE_MSVC_RUNTIME_LIBRARY_DLL"] = not is_msvc_static_runtime(self) tc.cache_variables["CMAKE_POLICY_DEFAULT_CMP0077"] = "NEW" diff --git a/pyDulcificum/pyDulcificum.cpp b/pyDulcificum/pyDulcificum.cpp new file mode 100644 index 0000000..192f463 --- /dev/null +++ b/pyDulcificum/pyDulcificum.cpp @@ -0,0 +1,25 @@ +#include +#include +#include + +namespace py = pybind11; + +PYBIND11_MODULE(pyDulcificum, module) +{ + module.doc() = R"pbdoc(exit + pyDulcificum + ----------------------- + .. currentmodule:: Python bindings for Dulcificum + .. autosummary:: + :toctree: _generate + add + subtract + )pbdoc"; + + py::class_(module, "Move") + .def(py::init<>()) + .def_readwrite("point", &dulcificum::botcmd::Move::point, "The point of the move command") + .def_readwrite("feedrate", &dulcificum::botcmd::Move::feedrate, "The feedrate of the move command") + .def_readwrite("is_point_relative", &dulcificum::botcmd::Move::is_point_relative, "Components of the point relative") + ; +} \ No newline at end of file From e0bb17c4d674f485e4a89724980c9dfdf998a858 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 12 Oct 2023 12:43:03 +0200 Subject: [PATCH 15/25] improved readability according to code-style Contributes to CURA-10561 --- .../mgjtp_mappings_json_key_to_str.h | 6 +-- src/miracle_jtp/mgjtp_command_to_json.cpp | 38 ++++++------- src/miracle_jtp/mgjtp_json_to_command.cpp | 54 +++++++++---------- 3 files changed, 49 insertions(+), 49 deletions(-) diff --git a/include/dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h b/include/dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h index 62a8afd..13adddf 100644 --- a/include/dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h +++ b/include/dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h @@ -41,7 +41,7 @@ NLOHMANN_JSON_SERIALIZE_ENUM( } // namespace botcmd -namespace miracle_jtp::kKeyStr +namespace miracle_jtp::k_key_str { constexpr static std::string_view a{ "a" }; @@ -60,9 +60,9 @@ constexpr static std::string_view value{ "value" }; constexpr static std::string_view x{ "x" }; constexpr static std::string_view y{ "y" }; constexpr static std::string_view z{ "z" }; -constexpr static std::array kParamPointNames{ x, y, z, a, b }; +constexpr static std::array k_param_point_names{ x, y, z, a, b }; -} // namespace miracle_jtp::kKeyStr +} // namespace miracle_jtp::k_key_str } // namespace dulcificum diff --git a/src/miracle_jtp/mgjtp_command_to_json.cpp b/src/miracle_jtp/mgjtp_command_to_json.cpp index 784a64a..cfa916b 100644 --- a/src/miracle_jtp/mgjtp_command_to_json.cpp +++ b/src/miracle_jtp/mgjtp_command_to_json.cpp @@ -39,7 +39,7 @@ nlohmann::json getCommandMetadata(const Command& cmd) if (cmd.type == CommandType::kMove) { auto move = static_cast(cmd); - jmetadata["relative"] = zipListsToJson(kKeyStr::kParamPointNames, move.is_point_relative); + jmetadata["relative"] = zipListsToJson(k_key_str::k_param_point_names, move.is_point_relative); return jmetadata; } return jmetadata; @@ -51,54 +51,54 @@ nlohmann::json getCommandParameters(const Command& cmd) if (cmd.type == CommandType::kMove) { const auto move = static_cast(cmd); - jparams = zipListsToJson(kKeyStr::kParamPointNames, move.point); - jparams[kKeyStr::feedrate] = move.feedrate; + jparams = zipListsToJson(k_key_str::k_param_point_names, move.point); + jparams[k_key_str::feedrate] = move.feedrate; return jparams; } if (cmd.type == CommandType::kComment) { const auto com = static_cast(cmd); - jparams[kKeyStr::comment] = com.comment; + jparams[k_key_str::comment] = com.comment; return jparams; } if (cmd.type == CommandType::kActiveFanDuty) { const auto dut = static_cast(cmd); - jparams[kKeyStr::index] = dut.index; - jparams[kKeyStr::value] = dut.duty; + jparams[k_key_str::index] = dut.index; + jparams[k_key_str::value] = dut.duty; return jparams; } if (cmd.type == CommandType::kActiveFanEnable) { const auto fan = static_cast(cmd); - jparams[kKeyStr::index] = fan.index; - jparams[kKeyStr::value] = fan.is_on; + jparams[k_key_str::index] = fan.index; + jparams[k_key_str::value] = fan.is_on; return jparams; } if (cmd.type == CommandType::kSetTemperature) { const auto dcmd = static_cast(cmd); - jparams[kKeyStr::index] = dcmd.index; - jparams[kKeyStr::temperature] = dcmd.temperature; + jparams[k_key_str::index] = dcmd.index; + jparams[k_key_str::temperature] = dcmd.temperature; return jparams; } if (cmd.type == CommandType::kWaitForTemperature) { const auto dcmd = static_cast(cmd); - jparams[kKeyStr::index] = dcmd.index; + jparams[k_key_str::index] = dcmd.index; return jparams; } if (cmd.type == CommandType::kChangeTool) { const auto dcmd = static_cast(cmd); - jparams = zipListsIgnoreNan(kKeyStr::kParamPointNames, dcmd.position); - jparams[kKeyStr::index] = dcmd.index; + jparams = zipListsIgnoreNan(k_key_str::k_param_point_names, dcmd.position); + jparams[k_key_str::index] = dcmd.index; return jparams; } if (cmd.type == CommandType::kDelay) { const auto dcmd = static_cast(cmd); - jparams[kKeyStr::seconds] = dcmd.seconds; + jparams[k_key_str::seconds] = dcmd.seconds; return jparams; } return jparams; @@ -107,12 +107,12 @@ nlohmann::json getCommandParameters(const Command& cmd) nlohmann::json toJson(const Command& cmd) { nlohmann::json jcmd; - jcmd[kKeyStr::function] = cmd.type; - jcmd[kKeyStr::metadata] = getCommandMetadata(cmd); - jcmd[kKeyStr::parameters] = getCommandParameters(cmd); - jcmd[kKeyStr::tags] = cmd.tags; + jcmd[k_key_str::function] = cmd.type; + jcmd[k_key_str::metadata] = getCommandMetadata(cmd); + jcmd[k_key_str::parameters] = getCommandParameters(cmd); + jcmd[k_key_str::tags] = cmd.tags; nlohmann::json jout; - jout[kKeyStr::command] = jcmd; + jout[k_key_str::command] = jcmd; return jout; } } // namespace dulcificum::miracle_jtp \ No newline at end of file diff --git a/src/miracle_jtp/mgjtp_json_to_command.cpp b/src/miracle_jtp/mgjtp_json_to_command.cpp index d90a8a2..72d3309 100644 --- a/src/miracle_jtp/mgjtp_json_to_command.cpp +++ b/src/miracle_jtp/mgjtp_json_to_command.cpp @@ -8,18 +8,18 @@ using namespace botcmd; std::shared_ptr toMove(const nlohmann::json& jmove) { auto move = std::make_shared(); - const auto& jparams = jmove.at(kKeyStr::parameters); - move->feedrate = jparams.at(kKeyStr::feedrate); - const auto& jrelative = jmove.at(kKeyStr::metadata).at("relative"); - for (size_t param_ii = 0; param_ii < kKeyStr::kParamPointNames.size(); param_ii++) + const auto& jparams = jmove.at(k_key_str::parameters); + move->feedrate = jparams.at(k_key_str::feedrate); + const auto& jrelative = jmove.at(k_key_str::metadata).at("relative"); + for (size_t param_ii = 0; param_ii < k_key_str::k_param_point_names.size(); param_ii++) { - const auto& key = kKeyStr::kParamPointNames[param_ii]; + const auto& key = k_key_str::k_param_point_names[param_ii]; double pval = jparams.at(key); move->point[param_ii] = pval; bool is_rel = jrelative.at(key); move->is_point_relative[param_ii] = is_rel; } - std::vector tags = jmove.at(kKeyStr::tags); + std::vector tags = jmove.at(k_key_str::tags); move->tags = std::move(tags); return move; } @@ -29,47 +29,47 @@ CommandPtr toParamOnlyCommand(const CommandType type, const nlohmann::json& jpar if (type == CommandType::kComment) { auto com = std::make_shared(); - com->comment = jparam.at(kKeyStr::comment); + com->comment = jparam.at(k_key_str::comment); return com; } if (type == CommandType::kActiveFanDuty) { auto cmd = std::make_shared(); - cmd->index = jparam[kKeyStr::index]; - cmd->duty = jparam[kKeyStr::value]; + cmd->index = jparam[k_key_str::index]; + cmd->duty = jparam[k_key_str::value]; return cmd; } if (type == CommandType::kActiveFanEnable) { auto cmd = std::make_shared(); - cmd->index = jparam[kKeyStr::index]; - cmd->is_on = jparam[kKeyStr::value]; + cmd->index = jparam[k_key_str::index]; + cmd->is_on = jparam[k_key_str::value]; return cmd; } if (type == CommandType::kSetTemperature) { auto cmd = std::make_shared(); - cmd->index = jparam[kKeyStr::index]; - cmd->temperature = jparam[kKeyStr::temperature]; + cmd->index = jparam[k_key_str::index]; + cmd->temperature = jparam[k_key_str::temperature]; return cmd; } if (type == CommandType::kWaitForTemperature) { auto cmd = std::make_shared(); - cmd->index = jparam[kKeyStr::index]; + cmd->index = jparam[k_key_str::index]; return cmd; } if (type == CommandType::kChangeTool) { auto cmd = std::make_shared(); - cmd->index = jparam[kKeyStr::index]; - cmd->temperature = jparam[kKeyStr::temperature]; + cmd->index = jparam[k_key_str::index]; + cmd->temperature = jparam[k_key_str::temperature]; return cmd; } if (type == CommandType::kDelay) { auto cmd = std::make_shared(); - cmd->seconds = jparam[kKeyStr::seconds]; + cmd->seconds = jparam[k_key_str::seconds]; return cmd; } return spawnCommandPtr(type); @@ -78,25 +78,25 @@ CommandPtr toParamOnlyCommand(const CommandType type, const nlohmann::json& jpar CommandPtr toChangeTool(const nlohmann::json& jcmd) { auto cmd = std::make_shared(); - auto jparam = jcmd.at(kKeyStr::parameters); - cmd->index = jparam.at(kKeyStr::index); - for (size_t param_ii = 0; param_ii < kKeyStr::kParamPointNames.size(); param_ii++) + auto jparam = jcmd.at(k_key_str::parameters); + cmd->index = jparam.at(k_key_str::index); + for (size_t param_ii = 0; param_ii < k_key_str::k_param_point_names.size(); param_ii++) { - const auto& key = kKeyStr::kParamPointNames[param_ii]; + const auto& key = k_key_str::k_param_point_names[param_ii]; if (jparam.contains(key)) { cmd->position[param_ii] = jparam.at(key); } } - std::vector tags = jcmd.at(kKeyStr::tags); + std::vector tags = jcmd.at(k_key_str::tags); cmd->tags = std::move(tags); return cmd; } CommandPtr toCommand(const nlohmann::json& jin) { - auto jcmd = jin[kKeyStr::command]; - CommandType type = jcmd[kKeyStr::function]; + auto jcmd = jin[k_key_str::command]; + CommandType type = jcmd[k_key_str::function]; if (type == CommandType::kMove) { return toMove(jcmd); @@ -105,11 +105,11 @@ CommandPtr toCommand(const nlohmann::json& jin) { return toChangeTool(jcmd); } - if (jcmd.contains(kKeyStr::parameters)) + if (jcmd.contains(k_key_str::parameters)) { - auto jparam = jcmd[kKeyStr::parameters]; + auto jparam = jcmd[k_key_str::parameters]; auto cmd = toParamOnlyCommand(type, jparam); - std::vector tags = jcmd.at(kKeyStr::tags); + std::vector tags = jcmd.at(k_key_str::tags); cmd->tags = std::move(tags); return cmd; } From 799b852f1497bfe6e4261dced32513752ab376ef Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 12 Oct 2023 12:46:41 +0200 Subject: [PATCH 16/25] Don't use namespace using-directives Contributes to CURA-10561 --- src/miracle_jtp/mgjtp_command_to_json.cpp | 43 +++++++++--------- src/miracle_jtp/mgjtp_json_to_command.cpp | 53 +++++++++++------------ 2 files changed, 47 insertions(+), 49 deletions(-) diff --git a/src/miracle_jtp/mgjtp_command_to_json.cpp b/src/miracle_jtp/mgjtp_command_to_json.cpp index cfa916b..92f1fb1 100644 --- a/src/miracle_jtp/mgjtp_command_to_json.cpp +++ b/src/miracle_jtp/mgjtp_command_to_json.cpp @@ -4,7 +4,6 @@ namespace dulcificum::miracle_jtp { -using namespace botcmd; template nlohmann::json zipListsToJson(const KeyListType& keys, const ValListType& vals) @@ -33,78 +32,78 @@ nlohmann::json zipListsIgnoreNan(const KeyListType& keys, const ValListType& val } -nlohmann::json getCommandMetadata(const Command& cmd) +nlohmann::json getCommandMetadata(const botcmd::Command& cmd) { nlohmann::json jmetadata({}); - if (cmd.type == CommandType::kMove) + if (cmd.type == botcmd::CommandType::kMove) { - auto move = static_cast(cmd); + auto move = static_cast(cmd); jmetadata["relative"] = zipListsToJson(k_key_str::k_param_point_names, move.is_point_relative); return jmetadata; } return jmetadata; } -nlohmann::json getCommandParameters(const Command& cmd) +nlohmann::json getCommandParameters(const botcmd::Command& cmd) { nlohmann::json jparams({}); - if (cmd.type == CommandType::kMove) + if (cmd.type == botcmd::CommandType::kMove) { - const auto move = static_cast(cmd); + const auto move = static_cast(cmd); jparams = zipListsToJson(k_key_str::k_param_point_names, move.point); jparams[k_key_str::feedrate] = move.feedrate; return jparams; } - if (cmd.type == CommandType::kComment) + if (cmd.type == botcmd::CommandType::kComment) { - const auto com = static_cast(cmd); + const auto com = static_cast(cmd); jparams[k_key_str::comment] = com.comment; return jparams; } - if (cmd.type == CommandType::kActiveFanDuty) + if (cmd.type == botcmd::CommandType::kActiveFanDuty) { - const auto dut = static_cast(cmd); + const auto dut = static_cast(cmd); jparams[k_key_str::index] = dut.index; jparams[k_key_str::value] = dut.duty; return jparams; } - if (cmd.type == CommandType::kActiveFanEnable) + if (cmd.type == botcmd::CommandType::kActiveFanEnable) { - const auto fan = static_cast(cmd); + const auto fan = static_cast(cmd); jparams[k_key_str::index] = fan.index; jparams[k_key_str::value] = fan.is_on; return jparams; } - if (cmd.type == CommandType::kSetTemperature) + if (cmd.type == botcmd::CommandType::kSetTemperature) { - const auto dcmd = static_cast(cmd); + const auto dcmd = static_cast(cmd); jparams[k_key_str::index] = dcmd.index; jparams[k_key_str::temperature] = dcmd.temperature; return jparams; } - if (cmd.type == CommandType::kWaitForTemperature) + if (cmd.type == botcmd::CommandType::kWaitForTemperature) { - const auto dcmd = static_cast(cmd); + const auto dcmd = static_cast(cmd); jparams[k_key_str::index] = dcmd.index; return jparams; } - if (cmd.type == CommandType::kChangeTool) + if (cmd.type == botcmd::CommandType::kChangeTool) { - const auto dcmd = static_cast(cmd); + const auto dcmd = static_cast(cmd); jparams = zipListsIgnoreNan(k_key_str::k_param_point_names, dcmd.position); jparams[k_key_str::index] = dcmd.index; return jparams; } - if (cmd.type == CommandType::kDelay) + if (cmd.type == botcmd::CommandType::kDelay) { - const auto dcmd = static_cast(cmd); + const auto dcmd = static_cast(cmd); jparams[k_key_str::seconds] = dcmd.seconds; return jparams; } return jparams; } -nlohmann::json toJson(const Command& cmd) +nlohmann::json toJson(const botcmd::Command& cmd) { nlohmann::json jcmd; jcmd[k_key_str::function] = cmd.type; diff --git a/src/miracle_jtp/mgjtp_json_to_command.cpp b/src/miracle_jtp/mgjtp_json_to_command.cpp index 72d3309..db1b79a 100644 --- a/src/miracle_jtp/mgjtp_json_to_command.cpp +++ b/src/miracle_jtp/mgjtp_json_to_command.cpp @@ -3,11 +3,10 @@ namespace dulcificum::miracle_jtp { -using namespace botcmd; -std::shared_ptr toMove(const nlohmann::json& jmove) +std::shared_ptr toMove(const nlohmann::json& jmove) { - auto move = std::make_shared(); + auto move = std::make_shared(); const auto& jparams = jmove.at(k_key_str::parameters); move->feedrate = jparams.at(k_key_str::feedrate); const auto& jrelative = jmove.at(k_key_str::metadata).at("relative"); @@ -19,65 +18,65 @@ std::shared_ptr toMove(const nlohmann::json& jmove) bool is_rel = jrelative.at(key); move->is_point_relative[param_ii] = is_rel; } - std::vector tags = jmove.at(k_key_str::tags); + std::vector tags = jmove.at(k_key_str::tags); move->tags = std::move(tags); return move; } -CommandPtr toParamOnlyCommand(const CommandType type, const nlohmann::json& jparam) +botcmd::CommandPtr toParamOnlyCommand(const botcmd::CommandType type, const nlohmann::json& jparam) { - if (type == CommandType::kComment) + if (type == botcmd::CommandType::kComment) { - auto com = std::make_shared(); + auto com = std::make_shared(); com->comment = jparam.at(k_key_str::comment); return com; } - if (type == CommandType::kActiveFanDuty) + if (type == botcmd::CommandType::kActiveFanDuty) { - auto cmd = std::make_shared(); + auto cmd = std::make_shared(); cmd->index = jparam[k_key_str::index]; cmd->duty = jparam[k_key_str::value]; return cmd; } - if (type == CommandType::kActiveFanEnable) + if (type == botcmd::CommandType::kActiveFanEnable) { - auto cmd = std::make_shared(); + auto cmd = std::make_shared(); cmd->index = jparam[k_key_str::index]; cmd->is_on = jparam[k_key_str::value]; return cmd; } - if (type == CommandType::kSetTemperature) + if (type == botcmd::CommandType::kSetTemperature) { - auto cmd = std::make_shared(); + auto cmd = std::make_shared(); cmd->index = jparam[k_key_str::index]; cmd->temperature = jparam[k_key_str::temperature]; return cmd; } - if (type == CommandType::kWaitForTemperature) + if (type == botcmd::CommandType::kWaitForTemperature) { - auto cmd = std::make_shared(); + auto cmd = std::make_shared(); cmd->index = jparam[k_key_str::index]; return cmd; } - if (type == CommandType::kChangeTool) + if (type == botcmd::CommandType::kChangeTool) { - auto cmd = std::make_shared(); + auto cmd = std::make_shared(); cmd->index = jparam[k_key_str::index]; cmd->temperature = jparam[k_key_str::temperature]; return cmd; } - if (type == CommandType::kDelay) + if (type == botcmd::CommandType::kDelay) { - auto cmd = std::make_shared(); + auto cmd = std::make_shared(); cmd->seconds = jparam[k_key_str::seconds]; return cmd; } return spawnCommandPtr(type); } -CommandPtr toChangeTool(const nlohmann::json& jcmd) +botcmd::CommandPtr toChangeTool(const nlohmann::json& jcmd) { - auto cmd = std::make_shared(); + auto cmd = std::make_shared(); auto jparam = jcmd.at(k_key_str::parameters); cmd->index = jparam.at(k_key_str::index); for (size_t param_ii = 0; param_ii < k_key_str::k_param_point_names.size(); param_ii++) @@ -88,20 +87,20 @@ CommandPtr toChangeTool(const nlohmann::json& jcmd) cmd->position[param_ii] = jparam.at(key); } } - std::vector tags = jcmd.at(k_key_str::tags); + std::vector tags = jcmd.at(k_key_str::tags); cmd->tags = std::move(tags); return cmd; } -CommandPtr toCommand(const nlohmann::json& jin) +botcmd::CommandPtr toCommand(const nlohmann::json& jin) { auto jcmd = jin[k_key_str::command]; - CommandType type = jcmd[k_key_str::function]; - if (type == CommandType::kMove) + botcmd::CommandType type = jcmd[k_key_str::function]; + if (type == botcmd::CommandType::kMove) { return toMove(jcmd); } - if (type == CommandType::kChangeTool) + if (type == botcmd::CommandType::kChangeTool) { return toChangeTool(jcmd); } @@ -109,7 +108,7 @@ CommandPtr toCommand(const nlohmann::json& jin) { auto jparam = jcmd[k_key_str::parameters]; auto cmd = toParamOnlyCommand(type, jparam); - std::vector tags = jcmd.at(k_key_str::tags); + std::vector tags = jcmd.at(k_key_str::tags); cmd->tags = std::move(tags); return cmd; } From 924a03cf6d4032436cd165cd0d3d9e49735987d1 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 12 Oct 2023 12:51:16 +0200 Subject: [PATCH 17/25] Fixed strip target Contributes to CURA-10561 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0c0dd78..e4cddf2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,7 +43,7 @@ if (WITH_PYTHON_BINDINGS) pybind11_add_module(pyDulcificum pyDulcificum/pyDulcificum.cpp) target_link_libraries(pyDulcificum PUBLIC dulcificum pybind11::pybind11 cpython::cpython) if(NOT MSVC AND NOT ${CMAKE_BUILD_TYPE} MATCHES Debug|RelWithDebInfo) - pybind11_strip(pyArcus) + pybind11_strip(pyDulcificum) endif() endif () From 231d0f6e21ab2d0430841059f94e4923cfb3b2ca Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 12 Oct 2023 14:31:30 +0200 Subject: [PATCH 18/25] Use ranged based for loops This commit adds the range-v3 library as a new dependency in the Conant and CMake files. This library is used to encapsulate iterators in ranged-based for loops for better readability and efficiency. It is specifically implemented in `mgjtp_json_to_command.cpp` and `mgjtp_command_to_json.cpp` files, where the traditional loops are replaced with ranged-based ones using `range/v3/view/zip.hpp` and `range/v3/view/remove_if.hpp`. This change simplifies the zipList functions in these files and improves code readability. Coontributes to CURA-10561 --- CMakeLists.txt | 2 ++ conanfile.py | 1 + src/miracle_jtp/mgjtp_command_to_json.cpp | 22 +++++++++++++--------- src/miracle_jtp/mgjtp_json_to_command.cpp | 2 ++ 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e4cddf2..57cc7bd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,7 @@ option(WITH_PYTHON_BINDINGS "Build with Python bindings: `pyDulcificum`" ON) find_package(nlohmann_json REQUIRED) find_package(spdlog REQUIRED) +find_package(range-v3 REQUIRED) # --- Setup the shared C++ mgjtp library --- set(DULCIFICUM_SRC @@ -21,6 +22,7 @@ target_link_libraries(dulcificum PUBLIC nlohmann_json::nlohmann_json PRIVATE + range-v3::range-v3 spdlog::spdlog) target_include_directories(dulcificum diff --git a/conanfile.py b/conanfile.py index d1c1890..3f11544 100644 --- a/conanfile.py +++ b/conanfile.py @@ -90,6 +90,7 @@ def layout(self): def requirements(self): self.requires("nlohmann_json/3.11.2", transitive_headers = True) + self.requires("range-v3/0.12.0") self.requires("spdlog/1.10.0") if self.options.with_apps: self.requires("docopt.cpp/0.6.3") diff --git a/src/miracle_jtp/mgjtp_command_to_json.cpp b/src/miracle_jtp/mgjtp_command_to_json.cpp index 92f1fb1..629d62c 100644 --- a/src/miracle_jtp/mgjtp_command_to_json.cpp +++ b/src/miracle_jtp/mgjtp_command_to_json.cpp @@ -2,6 +2,9 @@ #include "dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h" +#include +#include + namespace dulcificum::miracle_jtp { @@ -9,9 +12,9 @@ template nlohmann::json zipListsToJson(const KeyListType& keys, const ValListType& vals) { nlohmann::json jout; - for (size_t key_ii = 0; key_ii < keys.size(); key_ii++) + for (auto const& item : ranges::views::zip(keys, vals)) { - jout[keys[key_ii]] = vals[key_ii]; + jout[std::get<0>(item)] = std::get<1>(item); } return jout; } @@ -20,18 +23,19 @@ template nlohmann::json zipListsIgnoreNan(const KeyListType& keys, const ValListType& vals) { nlohmann::json jout; - for (size_t key_ii = 0; key_ii < keys.size(); key_ii++) + auto filter_key_value_view = ranges::views::zip(keys, vals) + | ranges::views::remove_if( + [](const auto& key_value) + { + return std::isnan(std::get<1>(key_value)); + }); + for (auto const& item : filter_key_value_view) { - const auto& val = vals[key_ii]; - if (! std::isnan(val)) - { - jout[keys[key_ii]] = vals[key_ii]; - } + jout[std::get<0>(item)] = std::get<1>(item); } return jout; } - nlohmann::json getCommandMetadata(const botcmd::Command& cmd) { nlohmann::json jmetadata({}); diff --git a/src/miracle_jtp/mgjtp_json_to_command.cpp b/src/miracle_jtp/mgjtp_json_to_command.cpp index db1b79a..66d86f0 100644 --- a/src/miracle_jtp/mgjtp_json_to_command.cpp +++ b/src/miracle_jtp/mgjtp_json_to_command.cpp @@ -1,6 +1,8 @@ #include "dulcificum/miracle_jtp/mgjtp_command_to_json.h" #include "dulcificum/miracle_jtp/mgjtp_mappings_json_key_to_str.h" +#include + namespace dulcificum::miracle_jtp { From 5bbc20542984b986c4ae0747f89fb2e1cf0f539f Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Thu, 12 Oct 2023 14:42:19 +0200 Subject: [PATCH 19/25] Use vector instead of array This is better supported by pybind11 out of the box, and it will also allow us to easily extend it for printers with more then 2 extruders. Contributes to CURA-10561 --- include/dulcificum/command_types.h | 5 ++--- pyDulcificum/pyDulcificum.cpp | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/include/dulcificum/command_types.h b/include/dulcificum/command_types.h index 0f0f3c4..099fcbd 100644 --- a/include/dulcificum/command_types.h +++ b/include/dulcificum/command_types.h @@ -1,7 +1,6 @@ #ifndef DULCIFICUM_COMMAND_TYPES_H #define DULCIFICUM_COMMAND_TYPES_H -#include #include #include #include @@ -11,7 +10,7 @@ namespace dulcificum { // x, y, z, a, b -using ParamPoint = std::array; +using ParamPoint = std::vector; namespace botcmd { @@ -82,7 +81,7 @@ struct Move : public Command ParamPoint point{ NAN, NAN, NAN, NAN, NAN }; double feedrate{ NAN }; - std::array is_point_relative{ false, false, false, true, true }; + std::vector is_point_relative{ false, false, false, true, true }; }; struct FanDuty : public Command diff --git a/pyDulcificum/pyDulcificum.cpp b/pyDulcificum/pyDulcificum.cpp index 192f463..66caa80 100644 --- a/pyDulcificum/pyDulcificum.cpp +++ b/pyDulcificum/pyDulcificum.cpp @@ -1,6 +1,7 @@ #include #include #include +#include namespace py = pybind11; From 76e0b5c0b1fbc11d51dcdf0675f0510b41bb7245 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Fri, 13 Oct 2023 07:22:05 +0200 Subject: [PATCH 20/25] Visualize the Griffin flavor GCode structure This commit introduces a new PlantUML file (griffin.puml) to illustrate the hierarchical structure of the UltiMaker g-code 'Griffin' flavor. It includes various classes like Griffin, Header, Body, Layer, Mesh, Feature, etc. along with their relationships and properties. This visual representation will aid in understanding and working with the Griffin gcode format. Contribute to CURA-10561 --- doc/griffin.png | Bin 0 -> 226090 bytes doc/griffin.puml | 261 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 doc/griffin.png create mode 100644 doc/griffin.puml diff --git a/doc/griffin.png b/doc/griffin.png new file mode 100644 index 0000000000000000000000000000000000000000..2176ee5f4b639ef90a1538c25a879cecb0eaeeca GIT binary patch literal 226090 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