├── ci ├── demo_sm.png ├── .run-cmake-format ├── colcon.pkg ├── .run-clang-format ├── .add_qt_ppa_bionic.sh ├── dependencies.repos ├── CHANGELOG.rst ├── package.xml ├── .github └── workflows │ ├── cmake_format.yml │ ├── clang_format.yml │ └── ubuntu_focal.yml ├── test ├── CMakeLists.txt ├── utest.cpp └── demo_sm.scxml ├── .clang-format ├── CMakeLists.txt ├── src ├── demo.cpp └── scxml_sm_interface.cpp ├── README.md ├── cmake └── FindTinyXML2.cmake ├── include └── scxml_core │ └── scxml_sm_interface.h └── .cmake-format /ci: -------------------------------------------------------------------------------- 1 | .github/workflows/ -------------------------------------------------------------------------------- /demo_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swri-robotics/ros_scxml/HEAD/demo_sm.png -------------------------------------------------------------------------------- /.run-cmake-format: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | find . \( -name CMakeLists.txt -o -name \*.cmake \) -exec cmake-format -i {} \; 3 | -------------------------------------------------------------------------------- /colcon.pkg: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": ["share/scxml_core/hook/ament_prefix_path.dsv", "share/scxml_core/hook/ros_package_path.dsv"] 3 | } 4 | -------------------------------------------------------------------------------- /.run-clang-format: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | find . -type f -regex '.*\.\(cpp\|hpp\|cc\|cxx\|h\|hxx\)' -exec clang-format -style=file -i {} \; 3 | -------------------------------------------------------------------------------- /.add_qt_ppa_bionic.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | add-apt-repository -y ppa:skycoder42/qt-modules 3 | apt-get update -qq 4 | apt-get install -y --no-install-recommends libqt5scxml-dev 5 | -------------------------------------------------------------------------------- /dependencies.repos: -------------------------------------------------------------------------------- 1 | - git: 2 | local-name: 'ros_industrial_cmake_boilerplate' 3 | uri: 'https://github.com/ros-industrial/ros_industrial_cmake_boilerplate.git' 4 | version: 0.2.15 5 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | Changelog for package scxml_core 3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 | 5 | 1.0.0 (2022-07-21) 6 | ------------------ 7 | * Edit SCXML map type def to add neighbor search functionality (`#35 `_) 8 | Co-authored-by: Michael Ripperger 9 | * Contributors: Lily Baye-Wallace 10 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | scxml_core 4 | 1.0.0 5 | The ros scxml package 6 | 7 | Jorge Nicho 8 | BSD3 9 | 10 | ros_industrial_cmake_boilerplate 11 | libqt5-core 12 | qtdeclarative5-dev 13 | tinyxml2 14 | 15 | 16 | cmake 17 | 18 | 19 | -------------------------------------------------------------------------------- /.github/workflows/cmake_format.yml: -------------------------------------------------------------------------------- 1 | name: CMake-Format 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | schedule: 9 | - cron: '0 5 * * *' 10 | 11 | jobs: 12 | cmake_format: 13 | name: CMake-Format 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v1 17 | 18 | - name: Run CMake format 19 | run: | 20 | sudo pip3 install cmakelang 21 | ./.run-cmake-format 22 | output=$(git diff) 23 | if [ -n "$output" ]; then exit 1; else exit 0; fi 24 | -------------------------------------------------------------------------------- /.github/workflows/clang_format.yml: -------------------------------------------------------------------------------- 1 | name: Clang-Format 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | schedule: 9 | - cron: '0 5 * * *' 10 | 11 | jobs: 12 | clang_format: 13 | name: Clang-Format 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v1 17 | 18 | - name: Run clang format 19 | run: | 20 | sudo apt update 21 | sudo apt install -y git clang-format 22 | ./.run-clang-format 23 | output=$(git diff) 24 | if [ -n "$output" ]; then exit 1; else exit 0; fi 25 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | find_package(Qt5Test REQUIRED) 2 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 3 | set(CMAKE_AUTOMOC ON) 4 | 5 | add_executable(${PROJECT_NAME}_utest utest.cpp) 6 | target_link_libraries(${PROJECT_NAME}_utest PRIVATE ${PROJECT_NAME} Qt5::Test) 7 | target_compile_definitions(${PROJECT_NAME}_utest PRIVATE SCXML_FILE="${CMAKE_CURRENT_SOURCE_DIR}/demo_sm.scxml") 8 | target_cxx_version(${PROJECT_NAME}_utest PRIVATE VERSION 14) 9 | 10 | add_test(NAME run_tests COMMAND ${PROJECT_NAME}_utest) 11 | 12 | install( 13 | TARGETS ${PROJECT_NAME}_utest 14 | RUNTIME DESTINATION bin/tests 15 | LIBRARY DESTINATION lib/tests 16 | ARCHIVE DESTINATION lib/tests) 17 | -------------------------------------------------------------------------------- /.github/workflows/ubuntu_focal.yml: -------------------------------------------------------------------------------- 1 | name: Ubuntu-Focal 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | schedule: 9 | - cron: '0 5 * * *' 10 | 11 | jobs: 12 | ci: 13 | name: Ubuntu-Focal 14 | runs-on: ubuntu-latest 15 | env: 16 | PROJECT_DIR: src/ros_scxml 17 | steps: 18 | - name: Prepare workspace 19 | working-directory: .. 20 | run: | 21 | mkdir -p ${{ env.PROJECT_DIR }} 22 | 23 | - uses: actions/checkout@v1 24 | with: 25 | path: ${{ env.PROJECT_DIR }} 26 | 27 | - name: Install dependencies 28 | working-directory: ../.. 29 | run: | 30 | sudo apt update -q 31 | sudo apt install -q -y clang-tidy python3 python3-pip 32 | sudo pip3 install -q --upgrade pip 33 | sudo pip3 install -q colcon-common-extensions vcstool rosdep 34 | vcs import src < ${{ env.PROJECT_DIR }}/dependencies.repos 35 | sudo rosdep init -q 36 | rosdep update -q 37 | rosdep install --from-paths src --ignore-src -r -y -q 38 | sudo apt install -q -y libqt5scxml5-dev libqt5scxml5-bin 39 | 40 | - name: Build 41 | working-directory: ../.. 42 | run: | 43 | colcon build --cmake-args -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON 44 | 45 | - name: Test 46 | working-directory: ../.. 47 | run: | 48 | colcon test 49 | colcon test-result 50 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: Google 3 | AccessModifierOffset: -2 4 | AlignEscapedNewlinesLeft: false 5 | AlignTrailingComments: true 6 | AlignAfterOpenBracket: Align 7 | AllowAllParametersOfDeclarationOnNextLine: false 8 | AllowShortFunctionsOnASingleLine: true 9 | AllowShortIfStatementsOnASingleLine: false 10 | AllowShortLoopsOnASingleLine: false 11 | AllowShortLoopsOnASingleLine: false 12 | AlwaysBreakBeforeMultilineStrings: false 13 | AlwaysBreakTemplateDeclarations: true 14 | BinPackArguments: false 15 | BinPackParameters: false 16 | BreakBeforeBinaryOperators: false 17 | BreakBeforeTernaryOperators: false 18 | BreakConstructorInitializersBeforeComma: true 19 | ColumnLimit: 120 20 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 21 | ConstructorInitializerIndentWidth: 2 22 | ContinuationIndentWidth: 4 23 | Cpp11BracedListStyle: false 24 | DerivePointerBinding: false 25 | ExperimentalAutoDetectBinPacking: false 26 | IndentCaseLabels: true 27 | IndentFunctionDeclarationAfterType: false 28 | IndentWidth: 2 29 | MaxEmptyLinesToKeep: 1 30 | NamespaceIndentation: None 31 | ObjCSpaceBeforeProtocolList: true 32 | PenaltyBreakBeforeFirstCallParameter: 19 33 | PenaltyBreakComment: 60 34 | PenaltyBreakFirstLessLess: 1000 35 | PenaltyBreakString: 1 36 | PenaltyExcessCharacter: 1000 37 | PenaltyReturnTypeOnItsOwnLine: 90 38 | PointerBindsToType: true 39 | SortIncludes: false 40 | SpaceAfterControlStatementKeyword: true 41 | SpaceAfterCStyleCast: false 42 | SpaceBeforeAssignmentOperators: true 43 | SpaceInEmptyParentheses: false 44 | SpacesBeforeTrailingComments: 2 45 | SpacesInAngles: false 46 | SpacesInCStyleCastParentheses: false 47 | SpacesInParentheses: false 48 | Standard: Auto 49 | TabWidth: 2 50 | UseTab: Never 51 | 52 | # Configure each individual brace in BraceWrapping 53 | BreakBeforeBraces: Custom 54 | 55 | # Control of individual brace wrapping cases 56 | BraceWrapping: { 57 | AfterClass: 'true' 58 | AfterControlStatement: 'true' 59 | AfterEnum : 'true' 60 | AfterFunction : 'true' 61 | AfterNamespace : 'true' 62 | AfterStruct : 'true' 63 | AfterUnion : 'true' 64 | BeforeCatch : 'true' 65 | BeforeElse : 'true' 66 | IndentBraces : 'false' 67 | } 68 | ... 69 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | 3 | # Extract package name and version from package.xml 4 | find_package(ros_industrial_cmake_boilerplate REQUIRED) 5 | extract_package_metadata(pkg) 6 | 7 | project(${pkg_extracted_name} VERSION ${pkg_extracted_version} LANGUAGES CXX) 8 | 9 | list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake/") 10 | 11 | find_package(Qt5 REQUIRED COMPONENTS Core Scxml) 12 | find_package(TinyXML2 REQUIRED) 13 | 14 | # ###################################################################################################################### 15 | # Build ## 16 | # ###################################################################################################################### 17 | 18 | add_library(${PROJECT_NAME} SHARED src/scxml_sm_interface.cpp) 19 | target_link_libraries(${PROJECT_NAME} PUBLIC Qt5::Scxml Qt5::Core ${TinyXML2_LIBRARIES}) 20 | target_include_directories(${PROJECT_NAME} PUBLIC "$" 21 | "$") 22 | target_cxx_version(${PROJECT_NAME} PUBLIC VERSION 14) 23 | 24 | # Demo executable 25 | add_executable(${PROJECT_NAME}_demo src/demo.cpp) 26 | target_link_libraries(${PROJECT_NAME}_demo PUBLIC ${PROJECT_NAME} Qt5::Core) 27 | target_cxx_version(${PROJECT_NAME}_demo PUBLIC VERSION 14) 28 | 29 | # ###################################################################################################################### 30 | # Install ## 31 | # ###################################################################################################################### 32 | 33 | install(DIRECTORY include/ DESTINATION include) 34 | 35 | install(FILES "${CMAKE_CURRENT_LIST_DIR}/cmake/FindTinyXML2.cmake" DESTINATION lib/cmake/${PROJECT_NAME}) 36 | 37 | configure_package(NAMESPACE scxml_core DEPENDENCIES "Qt5 REQUIRED COMPONENTS Core Scxml" TARGETS ${PROJECT_NAME} 38 | ${PROJECT_NAME}_demo) 39 | 40 | # ###################################################################################################################### 41 | # Test ## 42 | # ###################################################################################################################### 43 | 44 | if(BUILD_TESTING) 45 | enable_testing() 46 | add_subdirectory(test) 47 | endif() 48 | -------------------------------------------------------------------------------- /src/demo.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | int main(int argc, char** argv) 7 | { 8 | try 9 | { 10 | if (argc != 2) 11 | throw std::runtime_error("Please provide SCXML file location as first argument"); 12 | 13 | // Load the state machine 14 | scxml_core::ScxmlSMInterface interface(argv[1]); 15 | 16 | // Add a simple state callback for each state 17 | const scxml_core::StateTransitionMap map = interface.getStateTransitionMap(); 18 | for (auto it = map.begin(); it != map.end(); ++it) 19 | { 20 | QString state_name = it->first; 21 | auto cb = [state_name]() { std::cout << "Entered state '" << state_name.toStdString() << "'" << std::endl; }; 22 | interface.addOnEntryCallback(state_name, cb); 23 | } 24 | 25 | // Create the Qt application 26 | QCoreApplication app(argc, argv); 27 | 28 | std::cout << "Starting the state machine" << std::endl; 29 | interface.getSM()->setRunning(true); 30 | app.processEvents(); 31 | std::cout << "Enter the numeric index of the desired event to execute" << std::endl; 32 | 33 | while (true) 34 | { 35 | // Process the Qt events 36 | app.processEvents(); 37 | 38 | // Get the active state and available events 39 | QStringList active_states = interface.getSM()->activeStateNames(); 40 | const QString& current_state = active_states.at(0); 41 | std::set> available_events = map.at(current_state); 42 | 43 | std::stringstream ss; 44 | ss << "Available events: [ "; 45 | for (const auto& pair : available_events) 46 | { 47 | ss << pair.first.toStdString() << " "; 48 | } 49 | ss << "]"; 50 | 51 | // Get user input as to which event to execute 52 | bool done = false; 53 | while (!done) 54 | { 55 | std::cout << ss.str() << std::endl; 56 | 57 | // Get the character input 58 | std::string str; 59 | std::getline(std::cin, str); 60 | QString input(str.c_str()); 61 | 62 | try 63 | { 64 | for (const auto& pair : available_events) 65 | { 66 | if (pair.first == input) 67 | interface.submitEvent(input); 68 | done = true; 69 | } 70 | } 71 | catch (...) 72 | { 73 | std::cout << "Event name: " << input.toStdString() << " was not in the list of available events " 74 | << std::endl; 75 | } 76 | } 77 | } 78 | } 79 | catch (const std::exception& ex) 80 | { 81 | std::cerr << "Error: " << ex.what() << std::endl; 82 | return -1; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ros_scxml 2 | [![Build Status](https://travis-ci.com/swri-robotics/ros_scxml.svg?branch=master)](https://travis-ci.com/swri-robotics/ros_scxml) 3 | [![Github Issues](https://img.shields.io/github/issues/swri-robotics/ros_scxml.svg)](http://github.com/swri-robotics/ros_scxml/issues) 4 | 5 | [![license - bsd 2 clause](https://img.shields.io/:license-BSD%202--Clause-blue.svg)](https://opensource.org/licenses/BSD-2-Clause) 6 | 7 | Lightweight finite state machine library that uses the [SCXML](https://commons.apache.org/proper/commons-scxml/guide/scxml-documents.html) standard 8 | 9 | --- 10 | ## Prerequisites 11 | ### QT 5 12 | The `QScxml` module is available from `Qt` version 5.7 and higher and is currently only distributed on Ubuntu 20.04. Use the following command to install it: 13 | 14 | ```bash 15 | sudo apt install libqt5scxml5-dev libqt5scxml5-bin 16 | ``` 17 | 18 | If your distribution or version of `Qt` does not have the `QScxml` module there are several options for getting it: 19 | 20 | #### Qt Modules PPA (Recommended) 21 | [This PPA](https://launchpad.net/~skycoder42/+archive/ubuntu/qt-modules) provides binary distributions of the `QScxml` module for Ubuntu 17.10 and 18.04. 22 | These binaries install to the standard install directory and should be the same sub-version as the other `Qt` modules for the distribution. 23 | This the most straightforward solution for Ubuntu 17.10/18.04. 24 | To install, run: 25 | 26 | ```bash 27 | sudo add-apt-repository ppa:beineri/ppa:skycoder42/qt-modules 28 | sudo apt update 29 | sudo apt install libqt5scxml-dev 30 | ``` 31 | 32 | #### `/opt` Directory Qt Install (PPA) 33 | [This PPA](https://launchpad.net/~beineri) provides binary distributions of various versions of Qt for various operating systems. 34 | These binaries install to the `/opt` directory, which is not a standard search path for `cmake`. 35 | As such, several build and run environment variables need to be modified in order to use this distribution of `Qt`. 36 | Installing a version of `Qt` from this PPA alongside the system installation can also cause version tagging issues when compiling code that depends on `Qt`. 37 | To install, run: 38 | 39 | ```bash 40 | sudo add-apt-repository ppa:beineri/opt-qt-5.12.10-bionic 41 | sudo apt-get update 42 | sudo apt install qt512scxml 43 | ``` 44 | > Note: Edit command above for your desired version of `Qt` 45 | 46 | In order to make this installation find-able to `cmake`, several environment variables must be set. 47 | Locate your Qt installation directory in the `/opt` directory and set the environment variables as follows: 48 | 49 | ```bash 50 | export CMAKE_PREFIX_PATH=/opt/qt/lib/cmake:$CMAKE_PREFIX_PATH 51 | export LD_LIBRARY_PATH=/opt/qt/lib:/opt/qt/plugins:$LD_LIBRARY_PATH 52 | ``` 53 | 54 | #### Alternative Download (Qt Installer) 55 | The library can be downloaded from [here](http://download.qt.io/official_releases/qt/). Run the installation script with root access and follow the on screen instructions. 56 | -------------------------------------------------------------------------------- /cmake/FindTinyXML2.cmake: -------------------------------------------------------------------------------- 1 | # ###################################################################################################################### 2 | # 3 | # CMake script for finding TinyXML2. 4 | # 5 | # Input variables: 6 | # 7 | # * TinyXML2_ROOT_DIR (optional): When specified, header files and libraries will be searched for in 8 | # ${TinyXML2_ROOT_DIR}/include ${TinyXML2_ROOT_DIR}/libs respectively, and the default CMake search order will be 9 | # ignored. When unspecified, the default CMake search order is used. This variable can be specified either as a CMake 10 | # or environment variable. If both are set, preference is given to the CMake variable. Use this variable for finding 11 | # packages installed in a nonstandard location, or for enforcing that one of multiple package installations is picked 12 | # up. 13 | # 14 | # Cache variables (not intended to be used in CMakeLists.txt files) 15 | # 16 | # * TinyXML2_INCLUDE_DIR: Absolute path to package headers. 17 | # * TinyXML2_LIBRARY: Absolute path to library. 18 | # 19 | # Output variables: 20 | # 21 | # * TinyXML2_FOUND: Boolean that indicates if the package was found 22 | # * TinyXML2_INCLUDE_DIRS: Paths to the necessary header files 23 | # * TinyXML2_LIBRARIES: Package libraries 24 | # 25 | # Example usage: 26 | # 27 | # find_package(TinyXML2) if(NOT TinyXML2_FOUND) # Error handling endif() ... 28 | # include_directories(${TinyXML2_INCLUDE_DIRS} ...) ... target_link_libraries(my_target ${TinyXML2_LIBRARIES}) 29 | # 30 | # ###################################################################################################################### 31 | 32 | # Get package location hint from environment variable (if any) 33 | if(NOT TinyXML2_ROOT_DIR AND DEFINED ENV{TinyXML2_ROOT_DIR}) 34 | set(TinyXML2_ROOT_DIR "$ENV{TinyXML2_ROOT_DIR}" 35 | CACHE PATH "TinyXML2 base directory location (optional, used for nonstandard installation paths)") 36 | endif() 37 | 38 | # Search path for nonstandard package locations 39 | if(TinyXML2_ROOT_DIR) 40 | set(TinyXML2_INCLUDE_PATH PATHS "${TinyXML2_ROOT_DIR}/include" NO_DEFAULT_PATH) 41 | set(TinyXML2_LIBRARY_PATH PATHS "${TinyXML2_ROOT_DIR}/lib" NO_DEFAULT_PATH) 42 | endif() 43 | 44 | # Find headers and libraries 45 | find_path(TinyXML2_INCLUDE_DIR NAMES tinyxml2.h PATH_SUFFIXES "tinyxml2" ${TinyXML2_INCLUDE_PATH}) 46 | find_library(TinyXML2_LIBRARY NAMES tinyxml2 PATH_SUFFIXES "tinyxml2" ${TinyXML2_LIBRARY_PATH}) 47 | 48 | mark_as_advanced(TinyXML2_INCLUDE_DIR TinyXML2_LIBRARY) 49 | 50 | # Output variables generation 51 | include(FindPackageHandleStandardArgs) 52 | find_package_handle_standard_args( 53 | TinyXML2 54 | DEFAULT_MSG 55 | TinyXML2_LIBRARY 56 | TinyXML2_INCLUDE_DIR) 57 | 58 | set(TinyXML2_FOUND ${TINYXML2_FOUND}) # Enforce case-correctness: Set appropriately cased variable... 59 | unset(TINYXML2_FOUND) # ...and unset uppercase variable generated by find_package_handle_standard_args 60 | 61 | if(TinyXML2_FOUND) 62 | set(TinyXML2_INCLUDE_DIRS ${TinyXML2_INCLUDE_DIR}) 63 | set(TinyXML2_LIBRARIES ${TinyXML2_LIBRARY}) 64 | endif() 65 | -------------------------------------------------------------------------------- /include/scxml_core/scxml_sm_interface.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace scxml_core 9 | { 10 | /** @brief Container for states and their associated transitions */ 11 | using StateTransitionMap = std::map>>; 12 | 13 | /** @brief Creates a map of known states and transition events associated with those states */ 14 | StateTransitionMap getStateTransitionMap(const std::string& scxml_file); 15 | 16 | /** 17 | * @brief Interface for the QScxmlStateMachine class 18 | * @details The QScxmlStateMachine class does not provide much error feedback if callbacks are assigned to incorrect 19 | * states or if events are submitted to states that do not have associated transitions. This class maintains a map of 20 | * known states and transitions and ensures that basic state machine operations occur correctly 21 | */ 22 | class ScxmlSMInterface 23 | { 24 | public: 25 | ScxmlSMInterface(const std::string& scxml_file); 26 | /** 27 | * @brief Returns the state to which a desired transition leads, from the input state 28 | */ 29 | QString getNeighbor(const QString& state, const QString& transition); 30 | 31 | /** 32 | * @brief Returns the state to which a desired transition leads, from the current state (including any active 33 | * higher-level parent states) 34 | */ 35 | QString getActiveStateNeighbor(const QString& transition); 36 | 37 | /** 38 | * @brief Adds a callback to the input state that will be invoked on entry to the state 39 | * @param async - flag for executing the input callback asynchronously 40 | * @throws exception if the state does not exist in the state machine 41 | */ 42 | void addOnEntryCallback(const QString& state, const std::function& callback, bool async = false); 43 | 44 | /** 45 | * @brief Adds a callback to the input state that will be invoked when leaving the state 46 | * @throws exception if the state does not exist in the state machine 47 | */ 48 | void addOnExitCallback(const QString& state, const std::function& callback); 49 | 50 | /** 51 | * @brief Submits an event to move the state machine to a different state 52 | * @param force - force the submission of the event, even if the asynchronous task isn't finished 53 | * @return True if the asynchronous callback for the current state was finished and the event could be posted, false 54 | * otherwise 55 | * @throws if the event is not a valid transition 56 | */ 57 | bool submitEvent(const QString& event, bool force = false); 58 | 59 | inline const QScxmlStateMachine* getSM() const { return sm_; } 60 | inline QScxmlStateMachine* getSM() { return sm_; } 61 | inline StateTransitionMap getStateTransitionMap() const { return state_transition_map_; } 62 | 63 | /** @brief Provides access to the future of an asynchronous callback for the input state */ 64 | inline QFuture& getStateFuture(const QString& state) { return future_map_.at(state); } 65 | 66 | protected: 67 | QScxmlStateMachine* sm_; 68 | const StateTransitionMap state_transition_map_; 69 | std::map> future_map_; 70 | }; 71 | 72 | } // namespace scxml_core 73 | -------------------------------------------------------------------------------- /test/utest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | using namespace scxml_core; 10 | 11 | static const QList> SM_SEQUENCE{ 12 | { "trAborted", "st2Aborted" }, { "userClear", "st2Clearing" }, { "trStopped", "st2Stopped" }, 13 | { "userReset", "st3Reseting" }, { "trIdle", "st3Idle" }, { "userStart", "st3Starting" }, 14 | { "trExecute", "st3Execute" }, { "pause", "st2Pause" }, { "resume", "st3Execute" }, 15 | { "trExecuting", "st3Completing" }, { "trCompleting", "st3Complete" } 16 | }; 17 | 18 | class UTest : public QObject 19 | { 20 | Q_OBJECT 21 | 22 | private slots: 23 | void badInputs() 24 | { 25 | try 26 | { 27 | ScxmlSMInterface sm("bad_file"); 28 | 29 | // If we made it this far, we fail 30 | QVERIFY(false); 31 | } 32 | catch (const std::exception&) 33 | { 34 | } 35 | 36 | try 37 | { 38 | auto map = getStateTransitionMap("bad_file"); 39 | 40 | // If we made it this far, we fail 41 | QVERIFY(false); 42 | } 43 | catch (const std::exception&) 44 | { 45 | } 46 | } 47 | 48 | void badCallbacksAndEvents() 49 | { 50 | ScxmlSMInterface sm(SCXML_FILE); 51 | auto cb = []() {}; 52 | const QString bad_state_name("state_name_definitely_not_in_diagram"); 53 | QVERIFY_EXCEPTION_THROWN(sm.addOnEntryCallback(bad_state_name, cb), std::runtime_error); 54 | QVERIFY_EXCEPTION_THROWN(sm.addOnEntryCallback(bad_state_name, cb), std::runtime_error); 55 | 56 | const QString bad_event("event_definitely_not_in_diagram"); 57 | QVERIFY_EXCEPTION_THROWN(sm.submitEvent(bad_event), std::runtime_error); 58 | } 59 | 60 | void runArbitrarySequence() 61 | { 62 | ScxmlSMInterface sm(SCXML_FILE); 63 | const StateTransitionMap map = sm.getStateTransitionMap(); 64 | 65 | for (auto it = map.begin(); it != map.end(); ++it) 66 | { 67 | const QString& state_name = it->first; 68 | auto entry_cb = [state_name]() { std::cout << state_name.toStdString() << std::endl; }; 69 | sm.addOnEntryCallback(state_name, entry_cb); 70 | } 71 | 72 | // Start the state machine and allow some time for the Qt thread to register the start event 73 | sm.getSM()->setRunning(true); 74 | QTest::qWait(100); 75 | QVERIFY(sm.getSM()->isRunning()); 76 | 77 | // Issue some number of transitions 78 | for (int i = 0; i < 10; ++i) 79 | { 80 | QStringList active_states = sm.getSM()->activeStateNames(); 81 | QVERIFY(active_states.size() > 0); 82 | 83 | // Get the available events for the first active state 84 | const std::set> available_events = map.at(active_states.at(0)); 85 | if (available_events.empty()) 86 | { 87 | std::cout << "State '" << active_states.at(0).toStdString() << "' has no available events" << std::endl; 88 | break; 89 | } 90 | 91 | // Get the available events for the first active state 92 | std::set> action_ids = sm.getStateTransitionMap().at(active_states.first()); 93 | QVERIFY(!action_ids.empty()); 94 | 95 | // Choose a random event to submit 96 | static std::mt19937 gen(1); 97 | std::uniform_int_distribution dist(0, available_events.size() - 1); 98 | 99 | const QString event = std::next(available_events.begin(), dist(gen))->first; 100 | 101 | std::cout << "Event: " << event.toStdString() << std::endl; 102 | QVERIFY(sm.submitEvent(event)); 103 | QTest::qWait(100); 104 | } 105 | } 106 | 107 | void runPlannedSequence() 108 | { 109 | ScxmlSMInterface sm(SCXML_FILE); 110 | StateTransitionMap map = sm.getStateTransitionMap(); 111 | 112 | for (auto it = map.begin(); it != map.end(); ++it) 113 | { 114 | const QString& state_name = it->first; 115 | auto entry_cb = [state_name]() { std::cout << state_name.toStdString() << std::endl; }; 116 | sm.addOnEntryCallback(state_name, entry_cb); 117 | } 118 | 119 | // Start the state machine and allow some time for the Qt thread to register the start event 120 | sm.getSM()->setRunning(true); 121 | QTest::qWait(100); 122 | QVERIFY(sm.getSM()->isRunning()); 123 | 124 | // Submit the events and check that the state machine moves to the corresponding state 125 | for (int i = 0; i < SM_SEQUENCE.size(); ++i) 126 | { 127 | QVERIFY(sm.submitEvent(SM_SEQUENCE.at(i).first)); 128 | QTest::qWait(100); 129 | 130 | QStringList active_states = sm.getSM()->activeStateNames(); 131 | QVERIFY(active_states.at(0) == SM_SEQUENCE.at(i).second); 132 | } 133 | } 134 | 135 | void runPlannedSequenceAsync() 136 | { 137 | ScxmlSMInterface sm(SCXML_FILE); 138 | // Get only the lowest level children states 139 | const QStringList states = sm.getSM()->stateNames(true); 140 | 141 | for (const QString& state_name : states) 142 | { 143 | // Create an asynchronous callback 144 | auto entry_cb = [state_name]() { 145 | std::cout << state_name.toStdString() << std::endl; 146 | // Wait for a while 147 | std::this_thread::sleep_for(std::chrono::seconds(2)); 148 | std::cout << "Callback complete" << std::endl; 149 | }; 150 | 151 | // Add the entry callback 152 | sm.addOnEntryCallback(state_name, entry_cb, true); 153 | } 154 | 155 | // Start the state machine and allow some time for the Qt thread to register the start event 156 | sm.getSM()->setRunning(true); 157 | QTest::qWait(100); 158 | QVERIFY(sm.getSM()->isRunning()); 159 | 160 | // Submit the events and check that the state machine moves to the corresponding state 161 | for (int i = 0; i < SM_SEQUENCE.size(); ++i) 162 | { 163 | // Issue the transition a few times when we know that the entry callbacks are not complete 164 | for (int j = 0; j < 5; ++j) 165 | { 166 | QVERIFY(!sm.submitEvent(SM_SEQUENCE.at(i).first)); 167 | } 168 | 169 | // Wait for the task to finish 170 | QStringList active_states = sm.getSM()->activeStateNames(); 171 | sm.getStateFuture(active_states.at(0)).waitForFinished(); 172 | 173 | // Submit the event, now that we know the callback is complete 174 | QVERIFY(sm.submitEvent(SM_SEQUENCE.at(i).first)); 175 | QTest::qWait(100); 176 | 177 | // Check that the expected state is now active 178 | active_states = sm.getSM()->activeStateNames(); 179 | QVERIFY(active_states.at(0) == SM_SEQUENCE.at(i).second); 180 | } 181 | } 182 | }; 183 | 184 | QTEST_MAIN(UTest); 185 | #include "utest.moc" 186 | -------------------------------------------------------------------------------- /test/demo_sm.scxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /src/scxml_sm_interface.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | static const char* SCXML_ELEMENT = "scxml"; 7 | static const char* STATE_ELEMENT = "state"; 8 | static const char* STATE_ID_ATTRIBUTE = "id"; 9 | static const char* HISTORY_STATE_ELEMENT = "history"; 10 | static const char* HISTORY_STATE_ID_ATTRIBUTE = "id"; 11 | static const char* TRANSITION_ELEMENT = "transition"; 12 | static const char* EVENT_ATTRIBUTE = "event"; 13 | static const char* TARGET_ATTRIBUTE = "target"; 14 | 15 | /** 16 | * @brief checks if events exists based on a string search 17 | * @param Qstring event to find 18 | * @param set of [event,target] 19 | */ 20 | static bool eventExists(const QString& event, const std::set>& events) 21 | { 22 | return std::any_of( 23 | events.begin(), events.end(), [&event](const std::pair& pair) { return pair.first == event; }); 24 | } 25 | 26 | /** 27 | * @brief Recursively adds states and transitions to the map 28 | * @param state 29 | * @param map 30 | */ 31 | static void getStateTransitionsRecursive(tinyxml2::XMLElement* state, 32 | scxml_core::StateTransitionMap& map, 33 | std::set> inherited_events) 34 | { 35 | using namespace tinyxml2; 36 | 37 | // Get the ID of this state element 38 | QString state_id; 39 | { 40 | const char* id = state->Attribute(STATE_ID_ATTRIBUTE); 41 | if (!id) 42 | throw std::runtime_error("'" + std::string(STATE_ELEMENT) + "' element does not have '" + 43 | std::string(STATE_ID_ATTRIBUTE) + "' attribute"); 44 | 45 | state_id = QString(id); 46 | } 47 | 48 | // Add this state to the map with its inherited events 49 | map[state_id] = inherited_events; 50 | 51 | // Add all the transitions for this state 52 | XMLElement* transition = state->FirstChildElement(TRANSITION_ELEMENT); 53 | while (transition) 54 | { 55 | // Get the name of the event associated with this transition 56 | const char* event = transition->Attribute(EVENT_ATTRIBUTE); 57 | if (!event) 58 | throw std::runtime_error("'" + std::string(TRANSITION_ELEMENT) + "' element does not have '" + 59 | std::string(EVENT_ATTRIBUTE) + "' attribute"); 60 | 61 | inherited_events.emplace(QString(event), QString(transition->Attribute(TARGET_ATTRIBUTE))); 62 | 63 | // Add the event name to the map 64 | map[state_id] = inherited_events; 65 | 66 | // Get the next transition element 67 | transition = transition->NextSiblingElement(TRANSITION_ELEMENT); 68 | } 69 | 70 | // Recurse if this node has nested state elements 71 | XMLElement* child_state_element = state->FirstChildElement(STATE_ELEMENT); 72 | while (child_state_element) 73 | { 74 | getStateTransitionsRecursive(child_state_element, map, map.at(state_id)); 75 | child_state_element = child_state_element->NextSiblingElement(STATE_ELEMENT); 76 | } 77 | 78 | // Get the history states 79 | XMLElement* history = state->FirstChildElement(HISTORY_STATE_ELEMENT); 80 | while (history) 81 | { 82 | const char* id = history->Attribute(HISTORY_STATE_ID_ATTRIBUTE); 83 | if (!id) 84 | throw std::runtime_error("'" + std::string(HISTORY_STATE_ELEMENT) + "' element does not have '" + 85 | std::string(HISTORY_STATE_ID_ATTRIBUTE) + "' attribute"); 86 | 87 | // Add this state to the map 88 | map[QString(id)] = std::set>{}; 89 | 90 | // History states do not have transitions or nested states, so no need to recurse into it 91 | history = history->NextSiblingElement(HISTORY_STATE_ELEMENT); 92 | } 93 | } 94 | 95 | namespace scxml_core 96 | { 97 | StateTransitionMap getStateTransitionMap(const std::string& scxml_file) 98 | { 99 | using namespace tinyxml2; 100 | 101 | // Create an XML document 102 | XMLDocument doc; 103 | if (doc.LoadFile(scxml_file.c_str()) != XMLError::XML_SUCCESS) 104 | throw std::runtime_error("Failed to load document"); 105 | 106 | XMLElement* scxml = doc.FirstChildElement(SCXML_ELEMENT); 107 | if (!scxml) 108 | throw std::runtime_error("Scxml document contains no '" + std::string(SCXML_ELEMENT) + "' element"); 109 | 110 | XMLElement* state = scxml->FirstChildElement(STATE_ELEMENT); 111 | if (!state) 112 | throw std::runtime_error("'" + std::string(SCXML_ELEMENT) + "' has no child '" + std::string(STATE_ELEMENT) + 113 | "' elements"); 114 | 115 | // Call the recursive function on each of the top level states 116 | StateTransitionMap map; 117 | while (state) 118 | { 119 | getStateTransitionsRecursive(state, map, std::set>{}); 120 | state = state->NextSiblingElement(STATE_ELEMENT); 121 | } 122 | 123 | return map; 124 | } 125 | 126 | ScxmlSMInterface::ScxmlSMInterface(const std::string& scxml_file) 127 | : sm_(QScxmlStateMachine::fromFile(QString::fromStdString(scxml_file))) 128 | , state_transition_map_(scxml_core::getStateTransitionMap(scxml_file)) 129 | { 130 | if (!sm_) 131 | throw std::runtime_error("Failed to create state machine"); 132 | 133 | // Double check that the states in the state machine correspond to those in the map 134 | QStringList states = sm_->stateNames(false); 135 | for (auto it = state_transition_map_.begin(); it != state_transition_map_.end(); ++it) 136 | { 137 | if (!states.contains(it->first)) 138 | throw std::runtime_error("State machine does not contain state '" + it->first.toStdString() + "'"); 139 | } 140 | for (const QString& state : states) 141 | { 142 | if (state_transition_map_.find(state) == state_transition_map_.end()) 143 | throw std::runtime_error("State transition map does not contain state '" + state.toStdString() + "'"); 144 | } 145 | 146 | // Initialize the future map for each state 147 | for (const QString& state : states) 148 | { 149 | future_map_[state] = QFuture{}; 150 | } 151 | } 152 | 153 | QString ScxmlSMInterface::getNeighbor(const QString& state, const QString& transition) 154 | { 155 | for (auto& pair : state_transition_map_.at(state)) 156 | { 157 | if (pair.first == transition) 158 | { 159 | return pair.second; 160 | } 161 | } 162 | throw std::runtime_error("State '" + state.toStdString() + "' does not have a neighbor after transition '" + 163 | transition.toStdString() + "'"); 164 | } 165 | 166 | QString ScxmlSMInterface::getActiveStateNeighbor(const QString& transition) 167 | { 168 | const QStringList states = sm_->activeStateNames(false); 169 | 170 | // `activeStateNames` returns the list of active state names, starting with the highest level state and ending with 171 | // the leaf. Iterate over the active state names in reverse to start with the leaf state 172 | for (auto it = states.rbegin(); it != states.rend(); ++it) 173 | { 174 | try 175 | { 176 | return getNeighbor(*it, transition); 177 | } 178 | catch (const std::exception&) 179 | { 180 | } 181 | } 182 | throw std::runtime_error("Active state & parents do not have a neighbor after transition '" + 183 | transition.toStdString() + "'"); 184 | } 185 | 186 | void ScxmlSMInterface::addOnEntryCallback(const QString& state, const std::function& callback, bool async) 187 | { 188 | if (state_transition_map_.find(state) == state_transition_map_.end()) 189 | throw std::runtime_error("State '" + state.toStdString() + "' is not known"); 190 | 191 | if (async) 192 | { 193 | auto async_cb = [this, state, callback]() { this->future_map_[state] = QtConcurrent::run(callback); }; 194 | sm_->connectToState(state, QScxmlStateMachine::onEntry(async_cb)); 195 | } 196 | else 197 | { 198 | sm_->connectToState(state, QScxmlStateMachine::onEntry(callback)); 199 | } 200 | } 201 | 202 | void ScxmlSMInterface::addOnExitCallback(const QString& state, const std::function& callback) 203 | { 204 | if (state_transition_map_.find(state) == state_transition_map_.end()) 205 | throw std::runtime_error("State '" + state.toStdString() + "' is not known"); 206 | sm_->connectToState(state, QScxmlStateMachine::onExit(callback)); 207 | } 208 | 209 | bool ScxmlSMInterface::submitEvent(const QString& event, bool force) 210 | { 211 | QStringList active_states = sm_->activeStateNames(); 212 | 213 | // Ensure at least one of the active states has the specified transition 214 | auto it = std::find_if(active_states.begin(), active_states.end(), [this, event](const QString& state) -> bool { 215 | return eventExists(event, state_transition_map_.at(state)); 216 | }); 217 | 218 | if (it == active_states.end()) 219 | { 220 | std::stringstream ss; 221 | ss << "Transition '" << event.toStdString() << "' was not defined for active states [ "; 222 | for (const QString& state : active_states) 223 | { 224 | ss << state.toStdString() << " "; 225 | } 226 | ss << "]"; 227 | throw std::runtime_error(ss.str()); 228 | } 229 | 230 | // Make sure the asynchronous callbacks for all active states have been completed 231 | if (!force) 232 | { 233 | for (const QString& state : active_states) 234 | { 235 | if (eventExists(event, state_transition_map_.at(state))) 236 | { 237 | // Check if the asynchronous callback is finished before submitting the event 238 | if (!future_map_.at(state).isFinished()) 239 | return false; 240 | } 241 | } 242 | } 243 | 244 | // Submit the event 245 | sm_->submitEvent(event); 246 | return true; 247 | } 248 | 249 | } // namespace scxml_core 250 | -------------------------------------------------------------------------------- /.cmake-format: -------------------------------------------------------------------------------- 1 | #!/bin/python 2 | # ---------------------------------- 3 | # Options affecting listfile parsing 4 | # ---------------------------------- 5 | with section("parse"): 6 | 7 | # Specify structure for custom cmake functions 8 | additional_commands = { 9 | 'target_clang_tidy': { 10 | 'pargs': {'nargs': 1}, 11 | 'kwargs': { 12 | 'ARGUMENTS': '*', 13 | 'ENABLE': 1, 14 | } 15 | }, 16 | 'target_include_what_you_use': { 17 | 'pargs': {'nargs': 1}, 18 | 'kwargs': { 19 | 'ARGUMENTS': '*', 20 | 'ENABLE': 1, 21 | } 22 | }, 23 | 'include_what_you_use': { 24 | 'pargs': {'nargs': 0}, 25 | 'kwargs': { 26 | 'ARGUMENTS': '*', 27 | 'ENABLE': 1, 28 | } 29 | }, 30 | 'target_cppcheck': { 31 | 'pargs': {'nargs': 1}, 32 | 'kwargs': { 33 | 'ARGUMENTS': '*', 34 | 'ENABLE': 1, 35 | } 36 | }, 37 | 'cppcheck': { 38 | 'pargs': {'nargs': 0}, 39 | 'kwargs': { 40 | 'ARGUMENTS': '*', 41 | 'ENABLE': 1, 42 | } 43 | }, 44 | 'configure_package': { 45 | 'pargs': {'nargs': 0}, 46 | 'kwargs': { 47 | 'NAMESPACE': '?', 48 | 'TARGETS': '*', 49 | } 50 | }, 51 | 'add_gtest_discover_tests': { 52 | 'pargs': {'nargs': 1}, 53 | }, 54 | 'add_run_tests_target': { 55 | 'pargs': {'nargs': 0}, 56 | 'kwargs': { 57 | 'ENABLE': 1, 58 | } 59 | }, 60 | 'add_run_benchmark_target': { 61 | 'pargs': {'nargs': 0}, 62 | 'kwargs': { 63 | 'ENABLE': 1, 64 | } 65 | }, 66 | 'target_cxx_version': { 67 | 'pargs': {'nargs': 1}, 68 | 'kwargs': { 69 | 'INTERFACE': '+', 70 | 'PUBLIC': '+', 71 | 'PRIVATE': '+', 72 | 'VERSION': 1, 73 | } 74 | }, 75 | 'target_code_coverage': { 76 | 'pargs': {'nargs': 1}, 77 | 'kwargs': { 78 | 'AUTO': '+', 79 | 'ALL': '+', 80 | 'EXTERNAL': '+', 81 | 'PRIVATE': '+', 82 | 'PUBLIC': '+', 83 | 'INTERFACE': '+', 84 | 'ENABLE': 1, 85 | 'EXCLUDE': '*', 86 | } 87 | }, 88 | 'add_code_coverage': { 89 | 'pargs': {'nargs': 0}, 90 | 'kwargs': { 91 | 'ENABLE': 1, 92 | } 93 | }, 94 | 'add_code_coverage_all_targets': { 95 | 'pargs': {'nargs': 0}, 96 | 'kwargs': { 97 | 'ENABLE': 1, 98 | 'EXCLUDE': '*', 99 | } 100 | }, 101 | 'tesseract_cpack': { 102 | 'pargs': {'nargs': 0}, 103 | 'kwargs': { 104 | 'VERSION': 1, 105 | 'MAINTAINER': 1, 106 | 'DESCRIPTION': 1, 107 | 'LICENSE_FILE': 1, 108 | 'README_FILE': 1, 109 | 'LINUX_DEPENDS': '*', 110 | 'WINDOWS_DEPENDS': '*', 111 | } 112 | }, 113 | } 114 | 115 | # Override configurations per-command where available 116 | override_spec = {} 117 | 118 | # Specify variable tags. 119 | vartags = [] 120 | 121 | # Specify property tags. 122 | proptags = [] 123 | 124 | # ----------------------------- 125 | # Options affecting formatting. 126 | # ----------------------------- 127 | with section("format"): 128 | 129 | # Disable formatting entirely, making cmake-format a no-op 130 | disable = False 131 | 132 | # How wide to allow formatted cmake files 133 | line_width = 120 134 | 135 | # How many spaces to tab for indent 136 | tab_size = 2 137 | 138 | # If true, lines are indented using tab characters (utf-8 0x09) instead of 139 | # space characters (utf-8 0x20). In cases where the layout would 140 | # require a fractional tab character, the behavior of the fractional 141 | # indentation is governed by 142 | use_tabchars = False 143 | 144 | # If is True, then the value of this variable indicates how 145 | # fractional indentions are handled during whitespace replacement. If set to 146 | # 'use-space', fractional indentation is left as spaces (utf-8 0x20). If set 147 | # to `round-up` fractional indentation is replaced with a single tab character 148 | # (utf-8 0x09) effectively shifting the column to the next tabstop 149 | fractional_tab_policy = 'use-space' 150 | 151 | # If an argument group contains more than this many sub-groups (parg or kwarg 152 | # groups) then force it to a vertical layout. 153 | max_subgroups_hwrap = 3 154 | 155 | # If a positional argument group contains more than this many arguments, then 156 | # force it to a vertical layout. 157 | max_pargs_hwrap = 3 158 | 159 | # If a cmdline positional group consumes more than this many lines without 160 | # nesting, then invalidate the layout (and nest) 161 | max_rows_cmdline = 2 162 | 163 | # If true, separate flow control names from their parentheses with a space 164 | separate_ctrl_name_with_space = False 165 | 166 | # If true, separate function names from parentheses with a space 167 | separate_fn_name_with_space = False 168 | 169 | # If a statement is wrapped to more than one line, than dangle the closing 170 | # parenthesis on its own line. 171 | dangle_parens = False 172 | 173 | # If the trailing parenthesis must be 'dangled' on its on line, then align it 174 | # to this reference: `prefix`: the start of the statement, `prefix-indent`: 175 | # the start of the statement, plus one indentation level, `child`: align to 176 | # the column of the arguments 177 | dangle_align = 'prefix' 178 | 179 | # If the statement spelling length (including space and parenthesis) is 180 | # smaller than this amount, then force reject nested layouts. 181 | min_prefix_chars = 4 182 | 183 | # If the statement spelling length (including space and parenthesis) is larger 184 | # than the tab width by more than this amount, then force reject un-nested 185 | # layouts. 186 | max_prefix_chars = 10 187 | 188 | # If a candidate layout is wrapped horizontally but it exceeds this many 189 | # lines, then reject the layout. 190 | max_lines_hwrap = 2 191 | 192 | # What style line endings to use in the output. 193 | line_ending = 'unix' 194 | 195 | # Format command names consistently as 'lower' or 'upper' case 196 | command_case = 'canonical' 197 | 198 | # Format keywords consistently as 'lower' or 'upper' case 199 | keyword_case = 'unchanged' 200 | 201 | # A list of command names which should always be wrapped 202 | always_wrap = [] 203 | 204 | # If true, the argument lists which are known to be sortable will be sorted 205 | # lexicographicall 206 | enable_sort = True 207 | 208 | # If true, the parsers may infer whether or not an argument list is sortable 209 | # (without annotation). 210 | autosort = False 211 | 212 | # By default, if cmake-format cannot successfully fit everything into the 213 | # desired linewidth it will apply the last, most agressive attempt that it 214 | # made. If this flag is True, however, cmake-format will print error, exit 215 | # with non-zero status code, and write-out nothing 216 | require_valid_layout = False 217 | 218 | # A dictionary mapping layout nodes to a list of wrap decisions. See the 219 | # documentation for more information. 220 | layout_passes = {} 221 | 222 | # ------------------------------------------------ 223 | # Options affecting comment reflow and formatting. 224 | # ------------------------------------------------ 225 | with section("markup"): 226 | 227 | # What character to use for bulleted lists 228 | bullet_char = '*' 229 | 230 | # What character to use as punctuation after numerals in an enumerated list 231 | enum_char = '.' 232 | 233 | # If comment markup is enabled, don't reflow the first comment block in each 234 | # listfile. Use this to preserve formatting of your copyright/license 235 | # statements. 236 | first_comment_is_literal = False 237 | 238 | # If comment markup is enabled, don't reflow any comment block which matches 239 | # this (regex) pattern. Default is `None` (disabled). 240 | literal_comment_pattern = None 241 | 242 | # Regular expression to match preformat fences in comments default= 243 | # ``r'^\s*([`~]{3}[`~]*)(.*)$'`` 244 | fence_pattern = '^\\s*([`~]{3}[`~]*)(.*)$' 245 | 246 | # Regular expression to match rulers in comments default= 247 | # ``r'^\s*[^\w\s]{3}.*[^\w\s]{3}$'`` 248 | ruler_pattern = '^\\s*[^\\w\\s]{3}.*[^\\w\\s]{3}$' 249 | 250 | # If a comment line matches starts with this pattern then it is explicitly a 251 | # trailing comment for the preceeding argument. Default is '#<' 252 | explicit_trailing_pattern = '#<' 253 | 254 | # If a comment line starts with at least this many consecutive hash 255 | # characters, then don't lstrip() them off. This allows for lazy hash rulers 256 | # where the first hash char is not separated by space 257 | hashruler_min_length = 10 258 | 259 | # If true, then insert a space between the first hash char and remaining hash 260 | # chars in a hash ruler, and normalize its length to fill the column 261 | canonicalize_hashrulers = True 262 | 263 | # enable comment markup parsing and reflow 264 | enable_markup = True 265 | 266 | # ---------------------------- 267 | # Options affecting the linter 268 | # ---------------------------- 269 | with section("lint"): 270 | 271 | # a list of lint codes to disable 272 | disabled_codes = [] 273 | 274 | # regular expression pattern describing valid function names 275 | function_pattern = '[0-9a-z_]+' 276 | 277 | # regular expression pattern describing valid macro names 278 | macro_pattern = '[0-9A-Z_]+' 279 | 280 | # regular expression pattern describing valid names for variables with global 281 | # (cache) scope 282 | global_var_pattern = '[A-Z][0-9A-Z_]+' 283 | 284 | # regular expression pattern describing valid names for variables with global 285 | # scope (but internal semantic) 286 | internal_var_pattern = '_[A-Z][0-9A-Z_]+' 287 | 288 | # regular expression pattern describing valid names for variables with local 289 | # scope 290 | local_var_pattern = '[a-z][a-z0-9_]+' 291 | 292 | # regular expression pattern describing valid names for privatedirectory 293 | # variables 294 | private_var_pattern = '_[0-9a-z_]+' 295 | 296 | # regular expression pattern describing valid names for public directory 297 | # variables 298 | public_var_pattern = '[A-Z][0-9A-Z_]+' 299 | 300 | # regular expression pattern describing valid names for function/macro 301 | # arguments and loop variables. 302 | argument_var_pattern = '[a-z][a-z0-9_]+' 303 | 304 | # regular expression pattern describing valid names for keywords used in 305 | # functions or macros 306 | keyword_pattern = '[A-Z][0-9A-Z_]+' 307 | 308 | # In the heuristic for C0201, how many conditionals to match within a loop in 309 | # before considering the loop a parser. 310 | max_conditionals_custom_parser = 2 311 | 312 | # Require at least this many newlines between statements 313 | min_statement_spacing = 1 314 | 315 | # Require no more than this many newlines between statements 316 | max_statement_spacing = 2 317 | max_returns = 6 318 | max_branches = 12 319 | max_arguments = 5 320 | max_localvars = 15 321 | max_statements = 50 322 | 323 | # ------------------------------- 324 | # Options affecting file encoding 325 | # ------------------------------- 326 | with section("encode"): 327 | 328 | # If true, emit the unicode byte-order mark (BOM) at the start of the file 329 | emit_byteorder_mark = False 330 | 331 | # Specify the encoding of the input file. Defaults to utf-8 332 | input_encoding = 'utf-8' 333 | 334 | # Specify the encoding of the output file. Defaults to utf-8. Note that cmake 335 | # only claims to support utf-8 so be careful when using anything else 336 | output_encoding = 'utf-8' 337 | 338 | # ------------------------------------- 339 | # Miscellaneous configurations options. 340 | # ------------------------------------- 341 | with section("misc"): 342 | 343 | # A dictionary containing any per-command configuration overrides. Currently 344 | # only `command_case` is supported. 345 | per_command = {} 346 | 347 | --------------------------------------------------------------------------------