continue workshop

This commit is contained in:
Robin Müller 2022-09-28 18:35:15 +02:00
parent 03d613ec2c
commit 8082d4ae7a
No known key found for this signature in database
GPG Key ID: 11D4952C8CCEF814
6 changed files with 163 additions and 32 deletions

View File

@ -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

View File

@ -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.

View File

@ -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 ~<Class>() = default;
```
3. Add a abstract virtual function `performOperation`.
Abstract virtual functions look like this in general
```cpp
virtual <functionName>(...) = 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

View File

@ -1,17 +1,31 @@
#include <iostream>
#include "fsfw/serviceinterface.h"
#include "fsfw/FSFW.h"
#include <thread>
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;
}

View File

@ -0,0 +1,17 @@
#include <iostream>
#include <thread>
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();
}

View File

@ -0,0 +1,31 @@
#include <iostream>
#include <thread>
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;
}