continue workshop
This commit is contained in:
parent
03d613ec2c
commit
8082d4ae7a
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
17
start/tasks-srcs/main-01.cpp
Normal file
17
start/tasks-srcs/main-01.cpp
Normal 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();
|
||||
}
|
31
start/tasks-srcs/main-02.cpp
Normal file
31
start/tasks-srcs/main-02.cpp
Normal 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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user