diff --git a/FSFWConfig.h b/FSFWConfig.h index adf9912..d03ec3e 100644 --- a/FSFWConfig.h +++ b/FSFWConfig.h @@ -7,7 +7,7 @@ //! Used to determine whether C++ ostreams are used which can increase //! the binary size significantly. If this is disabled, //! the C stdio functions can be used alternatively -#define FSFW_CPP_OSTREAM_ENABLED 1 +#define FSFW_CPP_OSTREAM_ENABLED 0 //! More FSFW related printouts depending on level. Useful for development. #define FSFW_VERBOSE_LEVEL 1 diff --git a/README.md b/README.md index a28124c..3263f57 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,10 @@ git submodule update # Overview This workshop does an incremental build-up of a simple software which -is similar to an On-Board Software. +is similar to an On-Board Software. It is organised in chapters which have multiple +tasks. For each task, a solution source file will be provided. in a related subfolder with the +same name. -It is organised in chapters which have a task description. +It is recommended to have a basic understanding of C++ basics and object-oriented programming +in general before doing this workshop. There are various books and online resources available to +learn this. diff --git a/start/01-tasks.md b/start/01-tasks.md index 1c90b0e..f44d38b 100644 --- a/start/01-tasks.md +++ b/start/01-tasks.md @@ -28,13 +28,95 @@ After that, the code is transitioned to use the abstraction provided by the fram ## 1. Scheduling a basic task using the C++ `std::thread` API The goal of this task is to set up a basic thread which prints the following -string every second: "Executing Dummy Task". +string every second: "Hello World". - [std::thread API](https://en.cppreference.com/w/cpp/thread/thread) - [Delaying a thread](https://en.cppreference.com/w/cpp/thread/sleep_for) ## 2. Changing to the concept of executable objects +The goal of this task is to convert the code from task 1 so the [std::thread] API takes an +executable object to move to a more object oriented task approach. The printout of the thread +should remain the same. The executable objects should be named `MyExecutableObject`. It contains +one function called `periodicOperation` which performs the printout, and a static function which +takes the `MyExecutableObject` itself by reference and executes it in a permanent loop. + +The executable object should be passed into the [std::thread] directly. + +### Hints + + - [std::reference_wrapper](https://en.cppreference.com/w/cpp/utility/functional/reference_wrapper) + to pass references to the [std::thread] API + - [std::chrono::milliseconds](https://en.cppreference.com/w/cpp/chrono/duration) has a constructor + where an `uint32_t` can be used to create the duration from a custon number. + +### Subtasks + + 1. Create a class called `MyExecutableObject` with a `public` block. + 2. Add a static function called `executeTask` which expects itself (`MyExecutableObject& self`) as + a parameter with an empty implementation + 3. Add a regular method called `performOperation` which performs the printout + 4. Implement `executeTask`. This function uses the passed object and performs the scheduling + specific part by calling `self.performOperation` in a permanent loop with a delay between + calls. You can hardcode the delay to 1000ms for the first implementation. + 5. Add a constructor to `MyExecutableObject` which expects a millisecond delay + as an `uint32_t` and cache it as a member variable. Then use this member + variable in the `executeTask` implementation to make the task frequency configurable via the + constructor (ctor) parameter. + +With the conversion to executable object, we have reached a useful goal in object-oriented +programming (OOP) in general: The application logic inside `performOperation` is now decoupled +from the scheduling logic inside `executeTask`. This is also called seperation of concerns. + +## 3. Making the executable objects generic + +Our approach is useful buts lacks being generic as it relies on `std` library API. C++ as an OOP +language provides abstraction in form of interfaces, which can be used to have different types of +generic executable objects. Interfaces usually do not have a lot of source code on their own. They +describe a design contract a class should have which implements the interface. In general, the FSFW +relies heavily on subclassing and inheritance to provide adaptions point to users. + +We are going to refactor our `MyExecutableObject` by introducing an interface for any executable +object. We are then going to add a generic class which expects an object fulfilling this design +contract and then executes that object. + +Interfaces in C++ are implemented using +[abstract classes](https://en.cppreference.com/w/cpp/language/abstract_class) which only contains +pure virtual functions. + +### Subtasks + + 1. Create an interface called `MyExecutableObjectIF`. You can create this like a regular class. + As opposed to Java the differences between interfaces and classes are only by convention. + 2. In general, it is recommended to add a virtual destructor to an interface. It looks like this: + ```cpp + virtual ~() = default; + ``` + 3. Add a abstract virtual function `performOperation`. + Abstract virtual functions look like this in general + + ```cpp + virtual (...) = 0; + ``` + 4. Implement you custom interface for `MyExecutableObject` by re-using the exsiting + `performOperation` function. In general, when implementing + an interface or overriding a virtual function, it is recommended to add the `override` keyword + to the function delaration. We do not have seperation between source and header files for + our class yet, so you can add the `override` keyword after the function arguments and before + the implementation block. The compiler will throw a compile error if a function is declared + override but no base object function was actually overriden. This can prevent subtle bugs. + Please note that `MyExecutableObject` is actually now forced to implement the + `performOperation` function because that function is pure. The compiler makes sure we fulfill + the design contract specified by the interface + 5. Add a new class called `MyPeriodicTask`. Our executed object and the task abstraction + are now explicitely decoupled by using composition. Composition means that we have + a "has-a" relationship instead of a "is-a" relationship. In general, composition is preferable + to inheritance for flexible software designs. The new `MyPeriodicTask` class should + have a ctor which expects a `MyExecutableObjectIF` by reference. It caches that object + and exposes a `start` method to start the task + +## 3. Using the framework abstractions + Threads generally expect a function which is then directly executed. Sometimes, the execution of threads needs to be deferred. For example, this can be useful if the execution of tasks should only start after a certain condition. @@ -54,24 +136,7 @@ are then executed sequentially. This allows a granular design of executable task For example, important tasks get an own dedicated thread while other low priority objects are scheduled consecutively in another thread. -The goal of this task is to convert the code from task 1 so the [std::thread] -API takes an executable object to move to a more object oriented task approach. -The printout of the thread should remain the same. - -It is recommended to pass this executable object into the [std::thread] directly. - - - [std::reference_wrapper](https://en.cppreference.com/w/cpp/utility/functional/reference_wrapper) - to pass referneces to the [std::thread] API. - -As a bonus task, you can make your executable object implement a -[MyExecutableObjectIF] interface class. An interface class is -an [abstract class](https://en.cppreference.com/w/cpp/language/abstract_class) which -only contains pure virtual functions. As such, it can only be implemented by other -objects and describes a certain API contract an object has to fulfill. - -## 3. Using the framework abstractions - -As described before, the framework provides task abstraction with some advantages +The task abstractions have the following advantages: - Task execution can be deferred until an explicit `start` method is called - Same uniform API across multiple operating systems diff --git a/start/main.cpp b/start/main.cpp index 006b104..862b6d0 100644 --- a/start/main.cpp +++ b/start/main.cpp @@ -1,17 +1,31 @@ #include - -#include "fsfw/serviceinterface.h" -#include "fsfw/FSFW.h" +#include using namespace std; -#if FSFW_CPP_OSTREAM_ENABLED == 1 -ServiceInterfaceStream sif::debug("DEBUG", false); -ServiceInterfaceStream sif::info("INFO", false); -ServiceInterfaceStream sif::warning("WARNING", false); -ServiceInterfaceStream sif::error("ERROR", false, true, true); -#endif +class MyExecutableObject { +public: + MyExecutableObject(uint32_t delayMs): delayMs(delayMs) {} + + static void executeTask(MyExecutableObject& self) { + while(true) { + self.performOperation(); + this_thread::sleep_for(std::chrono::milliseconds(self.delayMs)); + } + } + + void performOperation() { + cout << "Hello World" << endl; + } +private: + uint32_t delayMs; +}; int main() { - cout << "hello world!" << endl; + MyExecutableObject myExecutableObject(1000); + std::thread thread( + MyExecutableObject::executeTask, + std::reference_wrapper(myExecutableObject)); + thread.join(); + return 0; } diff --git a/start/tasks-srcs/main-01.cpp b/start/tasks-srcs/main-01.cpp new file mode 100644 index 0000000..3c020eb --- /dev/null +++ b/start/tasks-srcs/main-01.cpp @@ -0,0 +1,17 @@ +#include +#include + +using namespace std; + +void mySimpleTask() { + using namespace std::chrono_literals; + while(true) { + cout << "Hello World" << endl; + this_thread::sleep_for(1000ms); + } +} + +int main() { + std::thread thread(mySimpleTask); + thread.join(); +} diff --git a/start/tasks-srcs/main-02.cpp b/start/tasks-srcs/main-02.cpp new file mode 100644 index 0000000..862b6d0 --- /dev/null +++ b/start/tasks-srcs/main-02.cpp @@ -0,0 +1,31 @@ +#include +#include + +using namespace std; + +class MyExecutableObject { +public: + MyExecutableObject(uint32_t delayMs): delayMs(delayMs) {} + + static void executeTask(MyExecutableObject& self) { + while(true) { + self.performOperation(); + this_thread::sleep_for(std::chrono::milliseconds(self.delayMs)); + } + } + + void performOperation() { + cout << "Hello World" << endl; + } +private: + uint32_t delayMs; +}; + +int main() { + MyExecutableObject myExecutableObject(1000); + std::thread thread( + MyExecutableObject::executeTask, + std::reference_wrapper(myExecutableObject)); + thread.join(); + return 0; +}