bocchino c9f857f23b Revise math tutorial
Add title header
Increment other section header indents by one
2021-12-07 11:36:50 -08:00

2292 lines
71 KiB
Markdown

# F' Math Component Tutorial
# Table of Contents
* <a href="#Introduction">1. Introduction</a>
* <a href="#The-MathOp-Type">2. The MathOp Type</a>
* <a href="#The-MathOp-Type_Construct-the-FPP-Model">2.1. Construct the FPP Model</a>
* <a href="#The-MathOp-Type_Add-the-Model-to-the-Project">2.2. Add the Model to the Project</a>
* <a href="#The-MathOp-Type_Build-the-Model">2.3. Build the Model</a>
* <a href="#The-MathOp-Type_Reference-Implementation">2.4. Reference Implementation</a>
* <a href="#The-MathOp-and-MathResult-Ports">3. The MathOp and MathResult Ports</a>
* <a href="#The-MathOp-and-MathResult-Ports_Construct-the-FPP-Model">3.1. Construct the FPP Model</a>
* <a href="#The-MathOp-and-MathResult-Ports_Add-the-Model-to-the-Project">3.2. Add the Model to the Project</a>
* <a href="#The-MathOp-and-MathResult-Ports_Build-the-Model">3.3. Build the Model</a>
* <a href="#The-MathOp-and-MathResult-Ports_Reference-Implementation">3.4. Reference Implementation</a>
* <a href="#The-MathSender-Component">4. The MathSender Component</a>
* <a href="#The-MathSender-Component_Construct-the-FPP-Model">4.1. Construct the FPP Model</a>
* <a href="#The-MathSender-Component_Add-the-Model-to-the-Project">4.2. Add the Model to the Project</a>
* <a href="#The-MathSender-Component_Build-the-Stub-Implementation">4.3. Build the Stub Implementation</a>
* <a href="#The-MathSender-Component_Complete-the-Implementation">4.4. Complete the Implementation</a>
* <a href="#The-MathSender-Component_Write-and-Run-Unit-Tests">4.5. Write and Run Unit Tests</a>
* <a href="#The-MathSender-Component_Write-and-Run-Unit-Tests_Set-Up-the-Unit-Test-Environment">4.5.1. Set Up the Unit Test Environment</a>
* <a href="#The-MathSender-Component_Write-and-Run-Unit-Tests_Write-and-Run-One-Test">4.5.2. Write and Run One Test</a>
* <a href="#The-MathSender-Component_Write-and-Run-Unit-Tests_Write-and-Run-More-Tests">4.5.3. Write and Run More Tests</a>
* <a href="#The-MathSender-Component_Write-and-Run-Unit-Tests_Exercise-Random-Testing">4.5.4. Exercise: Random Testing</a>
* <a href="#The-MathSender-Component_Reference-Implementation">4.6. Reference Implementation</a>
* <a href="#The-MathReceiver-Component">5. The MathReceiver Component</a>
* <a href="#The-MathReceiver-Component_Construct-the-FPP-Model">5.1. Construct the FPP Model</a>
* <a href="#The-MathReceiver-Component_Add-the-Model-to-the-Project">5.2. Add the Model to the Project</a>
* <a href="#The-MathReceiver-Component_Build-the-Stub-Implementation">5.3. Build the Stub Implementation</a>
* <a href="#The-MathReceiver-Component_Complete-the-Implementation">5.4. Complete the Implementation</a>
* <a href="#The-MathReceiver-Component_Write-and-Run-Unit-Tests">5.5. Write and Run Unit Tests</a>
* <a href="#The-MathReceiver-Component_Write-and-Run-Unit-Tests_Set-up-the-Unit-Test-Environment">5.5.1. Set up the Unit Test Environment</a>
* <a href="#The-MathReceiver-Component_Write-and-Run-Unit-Tests_Add-Helper-Code">5.5.2. Add Helper Code</a>
* <a href="#The-MathReceiver-Component_Write-and-Run-Unit-Tests_Write-and-Run-Tests">5.5.3. Write and Run Tests</a>
* <a href="#The-MathReceiver-Component_Reference-Implementation">5.6. Reference Implementation</a>
* <a href="#The-MathReceiver-Component_Exercises">5.7. Exercises</a>
* <a href="#The-MathReceiver-Component_Exercises_Adding-Telemetry">5.7.1. Adding Telemetry</a>
* <a href="#The-MathReceiver-Component_Exercises_Error-Handling">5.7.2. Error Handling</a>
* <a href="#Updating-the-Ref-Deployment">6. Updating the Ref Deployment</a>
* <a href="#Updating-the-Ref-Deployment_Defining-the-Component-Instances">6.1. Defining the Component Instances</a>
* <a href="#Updating-the-Ref-Deployment_Updating-the-Topology">6.2. Updating the Topology</a>
* <a href="#Updating-the-Ref-Deployment_Building-the-Ref-Deployment">6.3. Building the Ref Deployment</a>
* <a href="#Updating-the-Ref-Deployment_Visualizing-the-Ref-Topology">6.4. Visualizing the Ref Topology</a>
* <a href="#Updating-the-Ref-Deployment_Reference-Implementation">6.5. Reference Implementation</a>
* <a href="#Running-the-Ref-Deployment">7. Running the Ref Deployment</a>
* <a href="#Running-the-Ref-Deployment_Sending-a-Command">7.1. Sending a Command</a>
* <a href="#Running-the-Ref-Deployment_Checking-Events">7.2. Checking Events</a>
* <a href="#Running-the-Ref-Deployment_Checking-Telemetry">7.3. Checking Telemetry</a>
* <a href="#Running-the-Ref-Deployment_Setting-Parameters">7.4. Setting Parameters</a>
* <a href="#Running-the-Ref-Deployment_Saving-Parameters">7.5. Saving Parameters</a>
* <a href="#Running-the-Ref-Deployment_GDS-Logs">7.6. GDS Logs</a>
<a name="Introduction"></a>
## 1. Introduction
This tutorial shows how to develop, test, and deploy a simple topology
consisting of two components:
1. `MathSender`: A component that receives commands and forwards work to
`MathReceiver`.
1. `MathReceiver`: A component that carries out arithmetic operations and
returns the results to `MathSender`.
See the diagram below.
<a name="math-top"></a>
![A simple topology for arithmetic computation](png/top.png)
**What is covered:** The tutorial covers the following concepts:
1. Using the [FPP modeling language](https://fprime-community.github.io/fpp) to
specify the types and ports used by the components.
1. Using the F Prime build system to build the types and ports.
1. Developing the `MathSender` component: Specifying the component, building
the component, completing the C++ component implementation, and writing
component unit tests.
1. Developing the `MathReceiver` component.
1. Adding the new components and connections to the F Prime `Ref` application.
1. Using the F Prime Ground Data System (GDS) to run the updated `Ref`
application.
**Prerequisites:** This tutorial assumes the following:
1. Basic knowledge of Unix: How to navigate in a shell and execute programs.
1. Basic knowledge of git: How to create a branch.
1. Basic knowledge of C++, including class declarations, inheritance,
and virtual functions.
If you have not yet installed F Prime on your system, do so now.
Follow the installation guide at `INSTALL.md`
in the [F Prime git repository](https://github.com/nasa/fprime).
You may also wish to work through the Getting Started tutorial at
`docs/GettingStarted/Tutorial.md`.
**Git branch:** This tutorial is designed to work on the branch `release/v3.0.0`.
Working on this tutorial will modify some files under version control in the
F Prime git repository.
Therefore it is a good idea to do this work on a new branch.
For example:
```bash
git checkout release/v3.0.0
git checkout -b math-tutorial
```
If you wish, you can save your work by committing to this branch.
<a name="The-MathOp-Type"></a>
## 2. The MathOp Type
In F Prime, a **type definition** defines a kind of data that you can pass
between components or use in commands and telemetry.
For this tutorial, we need one type definition.
It defines an enumeration called `MathOp`, which
represents a mathematical operation.
We will add the specification for the `MathOp` type to the
`Ref` topology.
We will do this in three stages:
1. Construct the FPP model.
1. Add the model to the project.
1. Build the model.
<a name="The-MathOp-Type_Construct-the-FPP-Model"></a>
### 2.1. Construct the FPP Model
**Create the MathTypes directory:**
Go to the directory `Ref` at the top-level of the
F Prime repository and run `mkdir MathTypes`.
This step creates a new directory `Ref/MathTypes`.
This directory will contain our new type.
**Create the FPP model file:**
Now go into the directory `Ref/MathTypes`.
In that directory, create a file `MathTypes.fpp` with the following contents:
```fpp
module Ref {
@ A math operation
enum MathOp {
ADD @< Addition
SUB @< Subtraction
MUL @< Multiplication
DIV @< Division
}
}
```
You can do this by typing, or by copy-paste.
This file defines an enumeration or **enum** with enumerated constants `ADD`,
`SUB`, `MUL`, and `DIV`.
These four constants represent the operations of addition, subtraction,
multiplication, and division.
The enum also defines a type `MathOp`; the enumerated constants are the values
of this type.
For more information on enums, see [_The FPP User's
Guide_](https://fprime-community.github.io/fpp/fpp-users-guide.html#Defining-Enums).
The enum `MathTypes` resides in an FPP module `Ref`.
An FPP module is like a C++ namespace: it encloses several definitions, each of
which is qualified with the name of the module.
For more information on FPP modules, see [_The FPP User's
Guide_](https://fprime-community.github.io/fpp/fpp-users-guide.html#Defining-Modules).
The text following a symbol `@` or `@<` is called an **annotation**.
These annotations are carried through the parsing and become comments in the
generated code.
For more information, see [_The FPP User's
Guide_](https://fprime-community.github.io/fpp/fpp-users-guide.html#Writing-Comments-and-Annotations).
<a name="types_add"></a>
<a name="The-MathOp-Type_Add-the-Model-to-the-Project"></a>
### 2.2. Add the Model to the Project
**Create Ref/MathTypes/CMakeLists.txt:**
Create a file `Ref/MathTypes/CMakeLists.txt` with the following contents:
```cmake
set(SOURCE_FILES
"${CMAKE_CURRENT_LIST_DIR}/MathTypes.fpp"
)
register_fprime_module()
```
This code will tell the build system how to build the FPP model.
**Update Ref/CMakeLists.txt:**
Now we need to add the new directory to the `Ref` project.
To do that, open the file `Ref/CMakeLists.txt`.
This file should already exist; it was put there by the developers
of the `Ref` topology.
In this file, you should see several lines starting with `add_fprime_subdirectory`.
Immediately after the last of those lines, add the following new line:
```cmake
add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/MathTypes/")
```
<a name="types_build"></a>
<a name="The-MathOp-Type_Build-the-Model"></a>
### 2.3. Build the Model
**Run the build:**
Do the following:
1. Go to the directory `Ref/MathTypes`.
1. If you have not already run `fprime-util generate`, then do so now.
1. Run the command `fprime-util build`.
The output should indicate that the model built without any errors.
If not, try to identify and correct what is wrong,
either by deciphering the error output, or by going over the steps again.
If you get stuck, you can look at the
<a href="#types_ref">reference implementation</a>.
**Inspect the generated code:**
Now go to the directory `Ref/build-fprime-automatic-native/Ref/MathTypes`
(you may want to use `pushd`, or do this in a separate shell,
so you don't lose your current working directory).
The directory `build-fprime-automatic-native` is where all the
generated code lives for the "automatic native" build of the `Ref`
project.
Within that directory is a directory tree that mirrors the project
structure.
In particular, `Ref/build-fprime-automatic-native/Ref/MathTypes`
contains the generated code for `Ref/MathTypes`.
Run `ls`.
You should see something like this:
```bash
CMakeFiles MathOpEnumAc.cpp MathOpEnumAi.xml.prev cmake_install.cmake
Makefile MathOpEnumAc.hpp autocoder
```
The files `MathOpEnumAc.hpp` and
`MathOpEnumAc.cpp` are the auto-generated C++ files
corresponding to the `MathOp` enum.
You may wish to study the file `MathOpEnumAc.hpp`.
This file gives the interface to the C++ class `Ref::MathOp`.
All enum types have a similar auto-generated class
interface.
<a name="types_ref"></a>
<a name="The-MathOp-Type_Reference-Implementation"></a>
### 2.4. Reference Implementation
A reference implementation for this section is available at
`docs/Tutorials/MathComponent/MathTypes`.
To build this implementation from a clean repository,
do the following:
1. Go to the `Ref` directory.
1. Run `cp -R ../docs/Tutorials/MathComponent/MathTypes .`
1. Update `Ref/CMakeLists.txt` as stated <a href="#types_add">above</a>.
1. Follow the steps for <a href="#types_build">building the model</a>.
If you have modified the repo, revise the steps accordingly.
For example, switch git branches, use `git stash` to stash
your changes, or move `MathTypes` to another directory such
as `MathTypes-saved`.
<a name="ports"></a>
<a name="The-MathOp-and-MathResult-Ports"></a>
## 3. The MathOp and MathResult Ports
A **port** is the endpoint of a connection between
two components.
A **port definition** is like a function signature;
it defines the type of the data carried on a port.
For this tutorial, we need two port definitions:
* `MathOp` for sending an arithmetic operation request from
`MathSender` to `MathReceiver`.
* `MathResult` for sending the result of an arithmetic
operation from `MathReceiver` to `MathSender`.
We follow the same three steps as in the previous section.
<a name="The-MathOp-and-MathResult-Ports_Construct-the-FPP-Model"></a>
### 3.1. Construct the FPP Model
**Create the MathPorts directory:**
Go to the directory `Ref` at the top-level of the
F Prime repository and run `mkdir MathPorts`.
This directory will contain our new ports.
**Create the FPP model file:**
Now go into the directory `Ref/MathPorts`.
Create a file `MathPorts.fpp` with the following contents:
```fpp
module Ref {
@ Port for requesting an operation on two numbers
port MathOp(
val1: F32 @< The first operand
op: MathOp @< The operation
val2: F32 @< The second operand
)
@ Port for returning the result of a math operation
port MathResult(
result: F32 @< the result of the operation
)
}
```
This file defines the ports `MathOp` and `MathResult`.
`MathOp` has three formal parameters: a first operand, an
operation, and a second operand.
The operands have type `F32`, which represents a 32-bit
floating-point number.
The operation has type `MathOp`, which is the enum type
we defined in the previous section.
`MathResult` has a single formal parameter, the value of type `F32`
returned as the result of the operation.
For more information about port definitions, see
[_The FPP User's Guide_](https://fprime-community.github.io/fpp/fpp-users-guide.html#Defining-Ports).
<a name="The-MathOp-and-MathResult-Ports_Add-the-Model-to-the-Project"></a>
### 3.2. Add the Model to the Project
Add add the model
`Ref/MathPorts/MathPorts.fpp` to the `Ref` project.
Carry out the steps in the
<a href="#types_add">previous section</a>, after
substituting `MathPorts` for `MathTypes`.
<a name="The-MathOp-and-MathResult-Ports_Build-the-Model"></a>
### 3.3. Build the Model
Carry out the steps in the
<a href="#types_build">previous section</a>,
in directory `MathPorts` instead of `MathTypes`.
The generated code will go in
`Ref/build-fprime-automatic-native/Ref/MathPorts`.
For port definitions, the names of the auto-generated C++
files end in `PortAc.hpp` and `PortAc.cpp`.
You can look at this code if you wish.
However, the auto-generated C++ port files are used
by the autocoded component implementations (described below);
you won't ever program directly against their interfaces.
<a name="The-MathOp-and-MathResult-Ports_Reference-Implementation"></a>
### 3.4. Reference Implementation
A reference implementation for this section is available at
`docs/Tutorials/MathComponent/MathPorts`.
To build this implementation, follow the steps
described for <a href="#types_ref">`MathTypes`</a>.
<a name="math-sender"></name>
<a name="The-MathSender-Component"></a>
## 4. The MathSender Component
Now we can build and test the `MathSender` component.
There are five steps:
1. Construct the FPP model.
1. Add the model to the project.
1. Build the stub implementation.
1. Complete the implementation.
1. Write and run unit tests.
<a name="The-MathSender-Component_Construct-the-FPP-Model"></a>
### 4.1. Construct the FPP Model
**Create the MathSender directory:**
Go to the directory `Ref` at the top-level of the
F Prime repository.
Run `mkdir MathSender` to create a directory for the new component.
**Create the FPP model file:**
Now go into the directory `Ref/MathSender`.
Create a file `MathSender.fpp` with the following contents:
```fpp
module Ref {
@ Component for sending a math operation
active component MathSender {
# ----------------------------------------------------------------------
# General ports
# ----------------------------------------------------------------------
@ Port for sending the operation request
output port mathOpOut: MathOp
@ Port for receiving the result
async input port mathResultIn: MathResult
# ----------------------------------------------------------------------
# Special ports
# ----------------------------------------------------------------------
@ Command receive port
command recv port cmdIn
@ Command registration port
command reg port cmdRegOut
@ Command response port
command resp port cmdResponseOut
@ Event port
event port eventOut
@ Telemetry port
telemetry port tlmOut
@ Text event port
text event port textEventOut
@ Time get port
time get port timeGetOut
# ----------------------------------------------------------------------
# Commands
# ----------------------------------------------------------------------
@ Do a math operation
async command DO_MATH(
val1: F32 @< The first operand
op: MathOp @< The operation
val2: F32 @< The second operand
)
# ----------------------------------------------------------------------
# Events
# ----------------------------------------------------------------------
@ Math command received
event COMMAND_RECV(
val1: F32 @< The first operand
op: MathOp @< The operation
val2: F32 @< The second operand
) \
severity activity low \
format "Math command received: {f} {} {f}"
@ Received math result
event RESULT(
result: F32 @< The math result
) \
severity activity high \
format "Math result is {f}"
# ----------------------------------------------------------------------
# Telemetry
# ----------------------------------------------------------------------
@ The first value
telemetry VAL1: F32
@ The operation
telemetry OP: MathOp
@ The second value
telemetry VAL2: F32
@ The result
telemetry RESULT: F32
}
}
```
This code defines a component `Ref.MathSender`.
The component is **active**, which means it has its
own thread.
Inside the definition of the `MathSender` component are
several specifiers.
We have divided the specifiers into five groups:
1. **General ports:** These are user-defined ports for
application-specific functions.
There are two general ports: an output port `mathOpOut`
of type `MathOp` and an input port `mathResultIn` of
type `MathResult`.
Notice that these port specifiers use the ports that
we defined <a href="#ports">above</a>.
The input port is **asynchronous**.
This means that invoking the port (i.e., sending
data on the port) puts a message on a queue.
The handler runs later, on the thread of this component.
1. **Special ports:** These are ports that have a special
meaning in F Prime.
There are ports for registering commands with the dispatcher,
receiving commands, sending command responses, emitting
event reports, emitting telemetry, and getting the time.
1. **Commands:** These are commands sent from the ground
or from a sequencer and dispatched to this component.
There is one command `DO_MATH` for doing a math operation.
The command is asynchronous.
This means that when the command arrives, it goes on a queue
and its handler is later run on the thread of this component.
1. **Events:** These are event reports that this component
can emit.
There are two event reports, one for receiving a command
and one for receiving a result.
1. **Telemetry:** These are **channels** that define telemetry
points that the this component can emit.
There are four telemetry channels: three for the arguments
to the last command received and one for the last
result received.
For more information on defining components, see
[_The FPP User's Guide_](https://fprime-community.github.io/fpp/fpp-users-guide.html#Defining-Components).
<a name="math-sender_add-model"></a>
<a name="The-MathSender-Component_Add-the-Model-to-the-Project"></a>
### 4.2. Add the Model to the Project
**Create Ref/MathSender/CMakeLists.txt:**
Create a file `Ref/MathSender/CMakeLists.txt` with the following contents:
```cmake
# Register the standard build
set(SOURCE_FILES
"${CMAKE_CURRENT_LIST_DIR}/MathSender.cpp"
"${CMAKE_CURRENT_LIST_DIR}/MathSender.fpp"
)
register_fprime_module()
```
This code will tell the build system how to build the FPP model
and component implementation.
**Update Ref/CMakeLists.txt:**
Add `Ref/MathSender` to `Ref/CMakeLists.txt`, as we did
for <a href="#types_add">`Ref/MathTypes`</a>.
<a name="math-sender_build-stub"></a>
<a name="The-MathSender-Component_Build-the-Stub-Implementation"></a>
### 4.3. Build the Stub Implementation
**Run the build:**
Go into the directory `Ref/MathTypes`.
Run the following commands:
```bash
touch MathSender.cpp
fprime-util impl
```
The first command creates an empty file `MathSender.cpp`.
The build rules we wrote in the previous section expect
this file to be there.
After the second command, the build system should
run for a bit.
At the end there should be two new files
in the directory:
`MathSenderComponentImpl.cpp-template` and
`MathSenderComponentImpl.hpp-template`.
Run the following commands:
```bash
mv MathSenderComponentImpl.cpp-template MathSender.cpp
mv MathSenderComponentImpl.hpp-template MathSender.hpp
```
These commands produce a template, or stub implementation,
of the `MathSender` implementation class.
You will fill in this implementation class below.
Now run the command `fprime-util build --jobs 4`.
The model and the stub implementation should build.
The option `--jobs 4` says to use four cores for the build.
This should make the build go faster.
You can use any number after `--jobs`, up to the number
of cores available on your system.
**Inspect the generated code:**
The generated code resides in the directory
`Ref/fprime-build-automatic-native-ut/Ref/MathSender`.
You may wish to look over the file `MathSenderComponentAc.hpp`
to get an idea of the interface to the auto-generated
base class `MathSenderComponentBase`.
The `MathSender` implementation class is a derived class
of this base class.
<a name="The-MathSender-Component_Complete-the-Implementation"></a>
### 4.4. Complete the Implementation
Now we can complete the stub implementation.
In an editor, open the file `MathSender.cpp`.
**Fill in the DO_MATH command handler:**
You should see a stub handler for the `DO_MATH`
command that looks like this:
```c++
void MathSender ::
DO_MATH_cmdHandler(
const FwOpcodeType opCode,
const U32 cmdSeq,
F32 val1,
MathOp op,
F32 val2
)
{
// TODO
this->cmdResponse_out(opCode,cmdSeq,Fw::CmdResponse::OK);
}
```
The handler `DO_MATH_handler` is called when the `MathSender`
component receives a `DO_MATH` command.
This handler overrides the corresponding pure virtual
function in the auto-generated base class.
Fill in the handler so that it looks like this:
```c++
void MathSender ::
DO_MATH_cmdHandler(
const FwOpcodeType opCode,
const U32 cmdSeq,
F32 val1,
MathOp op,
F32 val2
)
{
this->tlmWrite_VAL1(val1);
this->tlmWrite_OP(op);
this->tlmWrite_VAL2(val2);
this->log_ACTIVITY_LO_COMMAND_RECV(val1, op, val2);
this->mathOpOut_out(0, val1, op, val2);
this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK);
}
```
The first two arguments to the handler function provide
the command opcode and the command sequence number
(a unique identifier generated by the command dispatcher).
The remaining arguments are supplied when the command is sent,
for example, from the F Prime ground data system (GDS).
The implementation code does the following:
1. Emit telemetry and events.
1. Invoke the `mathOpOut` port to request that `MathReceiver`
perform the operation.
1. Send a command response indicating success.
The command response goes out on the special port
`cmdResponseOut`.
In F Prime, every execution of a command handler must end by
sending a command response.
The proper behavior of other framework components (e.g., command
dispatcher, command sequencer) depends upon adherence to this rule.
**Check the build:**
Run `fprime-util build` again to make sure that everything still builds.
**Fill in the mathResultIn handler:**
You should see a stub handler for the `mathResultIn`
port that looks like this:
```c++
void MathSender ::
mathResultIn_handler(
const NATIVE_INT_TYPE portNum,
F32 result
)
{
// TODO
}
```
The handler `mathResultIn_handler` is called when the `MathReceiver`
component code returns a result by invoking the `mathResultIn` port.
Again the handler overrides the corresponding pure virtual
function in the auto-generated base class.
Fill in the handler so that it looks like this:
```c++
void MathSender ::
mathResultIn_handler(
const NATIVE_INT_TYPE portNum,
F32 result
)
{
this->tlmWrite_RESULT(result);
this->log_ACTIVITY_HI_RESULT(result);
}
```
The implementation code emits the result on the `RESULT`
telemetry channel and as a `RESULT` event report.
**Check the build:**
Run `fprime-util build`.
<a name="math-sender_unit"></a>
<a name="The-MathSender-Component_Write-and-Run-Unit-Tests"></a>
### 4.5. Write and Run Unit Tests
**Unit tests** are an important part of FSW development.
At the component level, unit tests typically invoke input ports, send commands,
and check for expected values on output ports (including telemetry and event
ports).
We will carry out the unit testing for the `MathSender` component
in three steps:
1. Set up the unit test environment
1. Write and run one unit test
1. Write and run additional unit tests
<a name="math-sender_unit_setup"></a>
<a name="The-MathSender-Component_Write-and-Run-Unit-Tests_Set-Up-the-Unit-Test-Environment"></a>
#### 4.5.1. Set Up the Unit Test Environment
**Create the stub Tester class:**
Do the following in directory `Ref/MathSender`:
1. Run `mkdir -p test/ut` to create the directory where
the unit tests will reside.
1. Run the command `fprime-util impl --ut`.
It should generate files `Tester.cpp` and `Tester.hpp`.
1. Move these files to the `test/ut` directory:
```bash
mv Tester.* test/ut
```
**Create a stub main.cpp file:**
Now go to the directory `Ref/MathSender/test/ut`.
In that directory, create a file `main.cpp` with the
following contents:
```c++
#include "Tester.hpp"
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
```
This file is a stub for running tests using the
[Google Test framework](https://github.com/google/googletest).
Right now there aren't any tests to run; we will add one
in the next section.
**Update Ref/MathSender/CMakeLists.txt:**
Go back to the directory `Ref/MathSender`.
Add the following lines to `CMakeLists.txt`:
```cmake
# Register the unit test build
set(UT_SOURCE_FILES
"${CMAKE_CURRENT_LIST_DIR}/MathSender.fpp"
"${CMAKE_CURRENT_LIST_DIR}/test/ut/Tester.cpp"
"${CMAKE_CURRENT_LIST_DIR}/test/ut/main.cpp"
)
register_fprime_ut()
```
This code tells the build system how to build
and run the unit tests.
**Run the build:**
Now we can check that the unit test build is working.
1. If you have not yet run `fprime-util generate --ut`,
then do so now.
This step generates the CMake build cache for the unit
tests.
1. Run `fprime-util build --ut`.
Everything should build without errors.
**Inspect the generated code:**
The generated code is located at
`Ref/build-fprime-automatic-native-ut/Ref/MathSender`.
This directory contains two auto-generated classes:
1. `MathSenderGTestBase`: This is the direct base
class of `Tester`.
It provides a test interface implemented with Google Test
macros.
1. `MathSenderTesterBase`: This is the direct base
class of `MathSenderGTestBase`.
It provides basic features such as histories of port
invocations.
It is not specific to Google Test, so you can
use this class without Google Test if desired.
You can look at the header files for these generated classes
to see what operations they provide.
In the next sections we will provide some example uses
of these operations.
<a name="The-MathSender-Component_Write-and-Run-Unit-Tests_Write-and-Run-One-Test"></a>
#### 4.5.2. Write and Run One Test
Now we will write a unit test that exercises the
`DO_MATH` command.
We will do this in three phases:
1. In the `Tester` class, add a helper function for sending the command and
checking the responses.
That way multiple tests can reuse the same code.
1. In the `Tester` class, write a test function that
calls the helper to run a test.
1. In the `main` function, write a Google Test macro
that invokes the test function.
1. Run the test.
**Add a helper function:**
Go into the directory `Ref/MathSender/test/ut`.
In the file `Tester.hpp`, add the following lines
to the section entitled "Helper methods":
```c++
//! Test a DO_MATH command
void testDoMath(MathOp op);
```
In the file `Tester.cpp`, add the corresponding
function body:
```c++
void Tester ::
testDoMath(MathOp op)
{
// Pick values
const F32 val1 = 2.0;
const F32 val2 = 3.0;
// Send the command
// pick a command sequence number
const U32 cmdSeq = 10;
// send DO_MATH command
this->sendCmd_DO_MATH(0, cmdSeq, val1, op, val2);
// retrieve the message from the message queue and dispatch the command to the handler
this->component.doDispatch();
// Verify command receipt and response
// verify command response was sent
ASSERT_CMD_RESPONSE_SIZE(1);
// verify the command response was correct as expected
ASSERT_CMD_RESPONSE(0, MathSenderComponentBase::OPCODE_DO_MATH, cmdSeq, Fw::CmdResponse::OK);
// Verify operation request on mathOpOut
// verify that that one output port was invoked overall
ASSERT_FROM_PORT_HISTORY_SIZE(1);
// verify that the math operation port was invoked once
ASSERT_from_mathOpOut_SIZE(1);
// verify the arguments of the operation port
ASSERT_from_mathOpOut(0, val1, op, val2);
// Verify telemetry
// verify that 3 channels were written
ASSERT_TLM_SIZE(3);
// verify that the desired telemetry values were sent once
ASSERT_TLM_VAL1_SIZE(1);
ASSERT_TLM_VAL2_SIZE(1);
ASSERT_TLM_OP_SIZE(1);
// verify that the correct telemetry values were sent
ASSERT_TLM_VAL1(0, val1);
ASSERT_TLM_VAL2(0, val2);
ASSERT_TLM_OP(0, op);
// Verify event reports
// verify that one event was sent
ASSERT_EVENTS_SIZE(1);
// verify the expected event was sent once
ASSERT_EVENTS_COMMAND_RECV_SIZE(1);
// verify the correct event arguments were sent
ASSERT_EVENTS_COMMAND_RECV(0, val1, op, val2);
}
```
This function is parameterized over different
operations.
It is divided into five sections: sending the command,
checking the command response, checking the output on
`mathOpOut`, checking telemetry, and checking events.
The comments explain what is happening in each section.
For further information about the F Prime unit test
interface, see the F Prime User's Guide.
Notice that after sending the command to the component, we call
the function `doDispatch` on the component.
We do this in order to simulate the behavior of the active
component in a unit test environment.
In a flight configuration, the component has its own thread,
and the thread blocks on the `doDispatch` call until another
thread puts a message on the queue.
In a unit test context, there is only one thread, so the pattern
is to place work on the queue and then call `doDispatch` on
the same thread.
There are a couple of pitfalls to watch out for with this pattern:
1. If you put work on the queue and forget to call `doDispatch`,
the work won't get dispatched.
Likely this will cause a unit test failure.
1. If you call `doDispatch` without putting work on the queue,
the unit test will block until you kill the process (e.g.,
with control-C).
**Write a test function:**
Next we will write a test function that calls
`testDoMath` to test an `ADD` operation.
In `Tester.hpp`, add the following line in the
section entitled "Tests":
```c++
//! Test an ADD command
void testAddCommand();
```
In `Tester.cpp`, add the corresponding function
body:
```c++
void Tester ::
testAddCommand()
{
this->testDoMath(MathOp::ADD);
}
```
This function calls `testDoMath` to test an `ADD` command.
**Write a test macro:**
Add the following code to the file `main.cpp`,
before the definition of the `main` function:
```c++
TEST(Nominal, AddCommand) {
Ref::Tester tester;
tester.testAddCommand();
}
```
The `TEST` macro is an instruction to Google Test to run a test.
`Nominal` is the name of a test suite.
We put this test in the `Nominal` suite because it addresses
nominal (expected) behavior.
`AddCommand` is the name of the test.
Inside the body of the macro, the first line declares a new
object `tester` of type `Tester`.
We typically declare a new object for each unit test, so that
each test starts in a fresh state.
The second line invokes the function `testAddCommand`
that we wrote in the previous section.
**Run the test:**
Go back to directory `Ref/MathSender`.
Run the command `fprime-util check`.
The build system should compile and run the unit
tests.
You should see output indicating that the test ran
and passed.
As an exercise, try the following:
1. Change the behavior of the component
so that it does something incorrect.
For example, try adding one to a telemetry
value before emitting it.
1. Rerun the test and observe what happens.
<a name="The-MathSender-Component_Write-and-Run-Unit-Tests_Write-and-Run-More-Tests"></a>
#### 4.5.3. Write and Run More Tests
**Add more command tests:**
Try to follow the pattern given in the previous
section to add three more tests, one each
for operations `SUB`, `MUL`, and `DIV`.
Most of the work should be done in the helper
that we already wrote.
Each new test requires just a short test function
and a short test macro.
Run the tests to make sure everything compiles and
the tests pass.
**Add a result test:**
Add a test for exercising the scenario in which the `MathReceiver`
component sends a result back to `MathSender`.
1. Add the following function signature in the "Tests"
section of to `Tester.hpp`:
```c++
//! Test receipt of a result
void testResult();
```
1. Add the corresponding function body in `Tester.cpp`:
```c++
void Tester ::
testResult()
{
// Generate an expected result
const F32 result = 10.0;
// reset all telemetry and port history
this->clearHistory();
// call result port with result
this->invoke_to_mathResultIn(0, result);
// retrieve the message from the message queue and dispatch the command to the handler
this->component.doDispatch();
// verify one telemetry value was written
ASSERT_TLM_SIZE(1);
// verify the desired telemetry channel was sent once
ASSERT_TLM_RESULT_SIZE(1);
// verify the values of the telemetry channel
ASSERT_TLM_RESULT(0, result);
// verify one event was sent
ASSERT_EVENTS_SIZE(1);
// verify the expected event was sent once
ASSERT_EVENTS_RESULT_SIZE(1);
// verify the expect value of the event
ASSERT_EVENTS_RESULT(0, result);
}
```
This code is similar to the helper function in the previous section.
The main difference is that it invokes a port directly
(the `mathResultIn` port) instead of sending a command.
1. Add the following test macro to `main.cpp`:
```c++
TEST(Nominal, Result) {
Ref::Tester tester;
tester.testResult();
}
```
1. Run the tests.
Again you can try altering something in the component code
to see what effect it has on the test output.
<a name="math-sender_exercise"></a>
<a name="The-MathSender-Component_Write-and-Run-Unit-Tests_Exercise-Random-Testing"></a>
#### 4.5.4. Exercise: Random Testing
F Prime provides a module called `STest`
that provides helper classes and functions for writing
unit tests.
As an exercise, use the interface provided by
`STest/STest/Pick.hpp` to pick random values to use in the
tests instead of using hard-coded values such as 2.0, 3.0,
and 10.
**Modifying the code:** You will need to do the following:
1. Add `#include "STest/Pick/Pick.hpp"` to `Tester.cpp`.
1. Add the following
line to `Ref/MathSender/CMakeLists.txt`, before `register_fprime_ut`:
```cmake
set(UT_MOD_DEPS STest)
```
This line tells the build system to make the unit test build
depend on the `STest` build module.
1. Add `#include STest/Random/Random.hpp` to `main.cpp`.
1. Add the following line to the `main` function of `main.cpp`,
just before the return statement:
```c++
STest::Random::seed();
```
This line seeds the random number generator used by STest.
**Running the tests:**
Recompile and rerun the tests.
Now go to
`Ref/build-fprime-automatic-native-ut/Ref/MathSender` and inspect the
file `seed-history`.
This file is a log of random seed values.
Each line represents the seed used in the corresponding run.
**Fixing the random seed:**
Sometimes you may want to run a test with a particular seed value,
e.g., for replay debugging.
To do this, put the seed value into a file `seed` in the same
directory as `seed-history`.
If the file `seed` exists, then STest will use the seed it contains instead
of generating a new seed.
Try the following:
1. Copy the last value _S_ of `seed-history` into `seed`.
1. In `Ref/MathSender`, re-run the unit tests a few times.
1. Inspect `Ref/build-fprime-automatic-native-ut/Ref/MathSender/seed-history`.
You should see that the value _S_ was used in the runs you just did
(corresponding to the last few entries in `seed-history`).
<a name="The-MathSender-Component_Reference-Implementation"></a>
### 4.6. Reference Implementation
A reference implementation for this section is available at
`docs/Tutorials/MathComponent/MathSender`.
<a name="The-MathReceiver-Component"></a>
## 5. The MathReceiver Component
Now we will build and test the `MathReceiver` component.
We will use the same five steps as for the
<a href="#math-sender">`MathSender` component</a>.
<a name="The-MathReceiver-Component_Construct-the-FPP-Model"></a>
### 5.1. Construct the FPP Model
**Create the MathReceiver directory:**
Create the directory `Ref/MathReceiver`.
**Create the FPP model file:**
In directory `Ref/MathReceiver`, create a file
`MathReceiver.fpp` with the following contents:
```fpp
module Ref {
@ Component for receiving and performing a math operation
queued component MathReceiver {
# ----------------------------------------------------------------------
# General ports
# ----------------------------------------------------------------------
@ Port for receiving the math operation
async input port mathOpIn: MathOp
@ Port for returning the math result
output port mathResultOut: MathResult
@ The rate group scheduler input
sync input port schedIn: Svc.Sched
# ----------------------------------------------------------------------
# Special ports
# ----------------------------------------------------------------------
@ Command receive
command recv port cmdIn
@ Command registration
command reg port cmdRegOut
@ Command response
command resp port cmdResponseOut
@ Event
event port eventOut
@ Parameter get
param get port prmGetOut
@ Parameter set
param set port prmSetOut
@ Telemetry
telemetry port tlmOut
@ Text event
text event port textEventOut
@ Time get
time get port timeGetOut
# ----------------------------------------------------------------------
# Parameters
# ----------------------------------------------------------------------
@ The multiplier in the math operation
param FACTOR: F32 default 1.0 id 0 \
set opcode 10 \
save opcode 11
# ----------------------------------------------------------------------
# Events
# ----------------------------------------------------------------------
@ Factor updated
event FACTOR_UPDATED(
val: F32 @< The factor value
) \
severity activity high \
id 0 \
format "Factor updated to {f}" \
throttle 3
@ Math operation performed
event OPERATION_PERFORMED(
val: MathOp @< The operation
) \
severity activity high \
id 1 \
format "{} operation performed"
@ Event throttle cleared
event THROTTLE_CLEARED \
severity activity high \
id 2 \
format "Event throttle cleared"
# ----------------------------------------------------------------------
# Commands
# ----------------------------------------------------------------------
@ Clear the event throttle
async command CLEAR_EVENT_THROTTLE \
opcode 0
# ----------------------------------------------------------------------
# Telemetry
# ----------------------------------------------------------------------
@ The operation
telemetry OPERATION: MathOp id 0
@ Multiplication factor
telemetry FACTOR: F32 id 1
}
}
```
This code defines a component `Ref.MathReceiver`.
The component is **queued**, which means it has a queue
but no thread.
Work occurs when the thread of another component invokes
the `schedIn` port of this component.
We have divided the specifiers of this component into six groups:
1. **General ports:** There are three ports:
an input port `mathOpIn` for receiving a math operation,
an output port `mathResultOut` for sending a math result, and
an input port `schedIn` for receiving invocations from the scheduler.
`mathOpIn` is asynchronous.
That means invocations of `mathOpIn` put messages on a queue.
`schedIn` is synchronous.
That means invocations of `schedIn` immediately call the
handler function to do work.
1. **Special ports:**
As before, there are special ports for commands, events, telemetry,
and time.
There are also special ports for getting and setting parameters.
We will explain the function of these ports below.
1. **Parameters:** There is one **parameter**.
A parameter is a constant that is configurable by command.
In this case there is one parameter `FACTOR`.
It has the default value 1.0 until its value is changed by command.
When doing math, the `MathReceiver` component performs the requested
operation and then multiplies by this factor.
For example, if the arguments of the `mathOpIn` port
are _v1_, `ADD`, and _v2_, and the factor is _f_,
then the result sent on `mathResultOut` is
_(v1 + v2) f_.
1. **Events:** There are three event reports:
1. `FACTOR_UPDATED`: Emitted when the `FACTOR` parameter
is updated by command.
This event is **throttled** to a limit of three.
That means that after the event is emitted three times
it will not be emitted any more, until the throttling
is cleared by command (see below).
1. `OPERATION_PERFORMED`: Emitted when this component
performs a math operation.
1. `THROTTLE_CLEARED`: Emitted when the event throttling
is cleared.
1. **Commands:** There is one command for clearing
the event throttle.
1. **Telemetry:**
There two telemetry channels: one for reporting
the last operation received and one for reporting
the factor parameter.
For the parameters, events, commands, and telemetry, we chose
to put in all the opcodes and identifiers explicitly.
These can also be left implicit, as in the `MathSender`
component example.
For more information, see
[_The FPP User's Guide_](https://fprime-community.github.io/fpp/fpp-users-guide.html#Defining-Components).
<a name="The-MathReceiver-Component_Add-the-Model-to-the-Project"></a>
### 5.2. Add the Model to the Project
Follow the steps given for the
<a href="#math-sender_add-model">`MathSender` component</a>.
<a name="The-MathReceiver-Component_Build-the-Stub-Implementation"></a>
### 5.3. Build the Stub Implementation
Follow the same steps as for the
<a href="#math-sender_build-stub">`MathSender` component</a>.
<a name="The-MathReceiver-Component_Complete-the-Implementation"></a>
### 5.4. Complete the Implementation
**Fill in the mathOpIn handler:**
In `MathReceiver.cpp`, complete the implementation of
`mathOpIn_handler` so that it looks like this:
```cpp
void MathReceiver ::
mathOpIn_handler(
const NATIVE_INT_TYPE portNum,
F32 val1,
const MathOp& op,
F32 val2
)
{
// Get the initial result
F32 res = 0.0;
switch (op.e) {
case MathOp::ADD:
res = val1 + val2;
break;
case MathOp::SUB:
res = val1 - val2;
break;
case MathOp::MUL:
res = val1 * val2;
break;
case MathOp::DIV:
res = val1 / val2;
break;
default:
FW_ASSERT(0, op.e);
break;
}
// Get the factor value
Fw::ParamValid valid;
F32 factor = paramGet_FACTOR(valid);
FW_ASSERT(
valid.e == Fw::ParamValid::VALID || valid.e == Fw::ParamValid::DEFAULT,
valid.e
);
// Multiply result by factor
res *= factor;
// Emit telemetry and events
this->log_ACTIVITY_HI_OPERATION_PERFORMED(op);
this->tlmWrite_OPERATION(op);
// Emit result
this->mathResultOut_out(0, res);
}
```
This code does the following:
1. Compute an initial result based on the input values and
the requested operation.
1. Get the value of the factor parameter.
Check that the value is a valid value from the parameter
database or a default parameter value.
1. Multiply the initial result by the factor to generate
the final result.
1. Emit telemetry and events.
1. Emit the result.
Note that in step 1, `op` is an enum (a C++ class type), and `op.e`
is the corresponding numeric value (an integer type).
Note also that in the `default` case we deliberately fail
an assertion.
This is a standard pattern for exhaustive case checking.
We should never hit the assertion.
If we do, then a bug has occurred: we missed a case.
**Fill in the schedIn handler:**
In `MathReceiver.cpp`, complete the implementation of
`schedIn_handler` so that it looks like this:
```c++
void MathReceiver ::
schedIn_handler(
const NATIVE_INT_TYPE portNum,
NATIVE_UINT_TYPE context
)
{
U32 numMsgs = this->m_queue.getNumMsgs();
for (U32 i = 0; i < numMsgs; ++i) {
(void) this->doDispatch();
}
}
```
This code dispatches all the messages on the queue.
Note that for a queued component, we have to do this
dispatch explicitly in the `schedIn` handler.
For an active component, the framework auto-generates
the dispatch code.
**Fill in the CLEAR_EVENT_THROTTLE command handler:**
In `MathReceiver.cpp`, complete the implementation of
`CLEAR_EVENT_THROTTLE_cmdHandler` so that it looks like this:
```c++
void MathReceiver ::
CLEAR_EVENT_THROTTLE_cmdHandler(
const FwOpcodeType opCode,
const U32 cmdSeq
)
{
// clear throttle
this->log_ACTIVITY_HI_FACTOR_UPDATED_ThrottleClear();
// send event that throttle is cleared
this->log_ACTIVITY_HI_THROTTLE_CLEARED();
// reply with completion status
this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK);
}
```
The call to `log_ACTIVITY_HI_FACTOR_UPDATED_ThrottleClear` clears
the throttling of the `FACTOR_UPDATED` event.
The next two lines send a notification event and send
a command response.
**Add a parameterUpdated function:**
Add the following function to `MathReceiver.cpp`.
You will need to add the corresponding function header
to `MathReceiver.hpp`.
```c++
void MathReceiver ::
parameterUpdated(FwPrmIdType id)
{
switch (id) {
case PARAMID_FACTOR: {
Fw::ParamValid valid;
F32 val = this->paramGet_FACTOR(valid);
FW_ASSERT(
valid.e == Fw::ParamValid::VALID || valid.e == Fw::ParamValid::DEFAULT,
valid.e
);
this->log_ACTIVITY_HI_FACTOR_UPDATED(val);
break;
}
default:
FW_ASSERT(0, id);
break;
}
}
```
This code implements an optional function that, if present,
is called when a parameter is updated by command.
The parameter identifier is passed in as the `id` argument
of the function.
Here we do the following:
1. If the parameter identifier is `PARAMID_FACTOR` (the parameter
identifier corresponding to the `FACTOR` parameter,
then get the parameter value and emit an event report.
1. Otherwise fail an assertion.
This code should never run, because there are no other
parameters.
<a name="The-MathReceiver-Component_Write-and-Run-Unit-Tests"></a>
### 5.5. Write and Run Unit Tests
<a name="The-MathReceiver-Component_Write-and-Run-Unit-Tests_Set-up-the-Unit-Test-Environment"></a>
#### 5.5.1. Set up the Unit Test Environment
1. Follow the steps given for the
<a href="#math-sender_unit_setup">`MathSender` component</a>.
1. Follow the steps given under **Modifying the code**
for the
<a href="#math-sender_exercise">random testing exercise</a>,
so that you can use STest to pick random values.
<a name="The-MathReceiver-Component_Write-and-Run-Unit-Tests_Add-Helper-Code"></a>
#### 5.5.2. Add Helper Code
**Add a ThrottleState enum class:**
Add the following code to the beginning of the
`Tester` class in `Tester.hpp`:
```c++
private:
// ----------------------------------------------------------------------
// Types
// ----------------------------------------------------------------------
enum class ThrottleState {
THROTTLED,
NOT_THROTTLED
};
```
This code defines a C++ enum class for recording whether an
event is throttled.
**Add helper functions:**
Add each of the functions described below to the
"Helper methods" section of `Tester.cpp`.
For each function, you must add
the corresponding function prototype to `Tester.hpp`.
After adding each function, compile the unit tests
to make sure that everything still compiles.
Fix any errors that occur.
Add a `pickF32Value` function.
```c++
F32 Tester ::
pickF32Value()
{
const F32 m = 10e6;
return m * (1.0 - 2 * STest::Pick::inUnitInterval());
}
```
This function picks a random `F32` value in the range
_[ -10^6, 10^6 ]_.
Add a `setFactor` function.
```c++
void Tester ::
setFactor(
F32 factor,
ThrottleState throttleState
)
{
// clear history
this->clearHistory();
// set the parameter
this->paramSet_FACTOR(factor, Fw::ParamValid::VALID);
const U32 instance = STest::Pick::any();
const U32 cmdSeq = STest::Pick::any();
this->paramSend_FACTOR(instance, cmdSeq);
if (throttleState == ThrottleState::NOT_THROTTLED) {
// verify the parameter update notification event was sent
ASSERT_EVENTS_SIZE(1);
ASSERT_EVENTS_FACTOR_UPDATED_SIZE(1);
ASSERT_EVENTS_FACTOR_UPDATED(0, factor);
}
else {
ASSERT_EVENTS_SIZE(0);
}
}
```
This function does the following:
1. Clear the test history.
1. Send a command to the component to set the `FACTOR` parameter
to the value `factor`.
1. If `throttleState` is `NOT_THROTTLED`, then check
that the event was emitted.
Otherwise check that the event was throttled (not emitted).
Add a function `computeResult` to `Tester.cpp`.
```c++
F32 Tester ::
computeResult(
F32 val1,
MathOp op,
F32 val2,
F32 factor
)
{
F32 result = 0;
switch (op.e) {
case MathOp::ADD:
result = val1 + val2;
break;
case MathOp::SUB:
result = val1 - val2;
break;
case MathOp::MUL:
result = val1 * val2;
break;
case MathOp::DIV:
result = val1 / val2;
break;
default:
FW_ASSERT(0, op.e);
break;
}
result *= factor;
return result;
}
```
This function carries out the math computation of the
math component.
By running this function and comparing, we can
check the output of the component.
Add a `doMathOp` function to `Tester.cpp`.
```c++
void Tester ::
doMathOp(
MathOp op,
F32 factor
)
{
// pick values
const F32 val1 = pickF32Value();
const F32 val2 = pickF32Value();
// clear history
this->clearHistory();
// invoke operation port with add operation
this->invoke_to_mathOpIn(0, val1, op, val2);
// invoke scheduler port to dispatch message
const U32 context = STest::Pick::any();
this->invoke_to_schedIn(0, context);
// verify the result of the operation was returned
// check that there was one port invocation
ASSERT_FROM_PORT_HISTORY_SIZE(1);
// check that the port we expected was invoked
ASSERT_from_mathResultOut_SIZE(1);
// check that the component performed the operation correctly
const F32 result = computeResult(val1, op, val2, factor);
ASSERT_from_mathResultOut(0, result);
// verify events
// check that there was one event
ASSERT_EVENTS_SIZE(1);
// check that it was the op event
ASSERT_EVENTS_OPERATION_PERFORMED_SIZE(1);
// check that the event has the correct argument
ASSERT_EVENTS_OPERATION_PERFORMED(0, op);
// verify telemetry
// check that one channel was written
ASSERT_TLM_SIZE(1);
// check that it was the op channel
ASSERT_TLM_OPERATION_SIZE(1);
// check for the correct value of the channel
ASSERT_TLM_OPERATION(0, op);
}
```
This function is similar to the `doMath` helper function that
we wrote for the `MathSender` component.
Notice that the method for invoking a port is different.
Since the component is queued, we don't call `doDispatch`
directly.
Instead we invoke `schedIn`.
<a name="The-MathReceiver-Component_Write-and-Run-Unit-Tests_Write-and-Run-Tests"></a>
#### 5.5.3. Write and Run Tests
For each of the tests described below, you must add the
corresponding function prototype to `Tester.hpp`
and the corresponding test macro to `main.cpp`.
If you can't remember how to do it, look back at the
`MathSender` examples.
After writing each test, run all the tests and make sure
that they pass.
**Write an ADD test:**
Add the following function to the "Tests" section of `Tester.cpp`:
```c++
void Tester ::
testAdd()
{
// Set the factor parameter by command
const F32 factor = pickF32Value();
this->setFactor(factor, ThrottleState::NOT_THROTTLED);
// Do the add operation
this->doMathOp(MathOp::ADD, factor);
}
```
This function calls the `setFactor` helper function
to set the factor parameter.
Then it calls the `doMathOp` function to
do a math operation.
**Write a SUB test:**
Add the following function to the "Tests" section of `Tester.cpp`:
```c++
void Tester ::
testSub()
{
// Set the factor parameter by loading parameters
const F32 factor = pickF32Value();
this->paramSet_FACTOR(factor, Fw::ParamValid::VALID);
this->component.loadParameters();
// Do the operation
this->doMathOp(MathOp::SUB, factor);
}
```
This test is similar to `testAdd`, but it shows
another way to set a parameter.
`testAdd` showed how to set a parameter by command.
You can also set a parameter by initialization, as follows:
1. Call the `paramSet` function as shown.
This function sets the parameter value in
the part of the test harness that mimics the behavior of the
parameter database component.
1. Call the `loadParameters` function as shown.
In flight, the function `loadParameters` is typically called at the
start of FSW to load the parameters from the database;
here it loads the parameters from the test harness.
There is no command to update a parameter, so `parameterUpdated`
is not called, and no event is emitted.
As before, after setting the parameter we call `doMathOp`
to do the operation.
**Write a MUL test:**
This test is the same as the ADD test, except that it
uses MUL instead of add.
**Write a DIV test:**
This test is the same as the SUB test, except that it
uses DIV instead of SUB.
**Write a throttle test:**
Add the following function to the "Tests" section of `Tester.cpp`:
```c++
void Tester ::
testThrottle()
{
// send the number of commands required to throttle the event
// Use the autocoded value so the unit test passes if the
// throttle value is changed
const F32 factor = pickF32Value();
for (
U16 cycle = 0;
cycle < MathReceiverComponentBase::EVENTID_FACTOR_UPDATED_THROTTLE;
cycle++
) {
this->setFactor(factor, ThrottleState::NOT_THROTTLED);
}
// Event should now be throttled
this->setFactor(factor, ThrottleState::THROTTLED);
// send the command to clear the throttle
this->sendCmd_CLEAR_EVENT_THROTTLE(INSTANCE, CMD_SEQ);
// invoke scheduler port to dispatch message
const U32 context = STest::Pick::any();
this->invoke_to_schedIn(0, context);
// verify clear event was sent
ASSERT_EVENTS_SIZE(1);
ASSERT_EVENTS_THROTTLE_CLEARED_SIZE(1);
// Throttling should be cleared
this->setFactor(factor, ThrottleState::NOT_THROTTLED);
}
```
This test first loops over the throttle count, which is stored
for us in the constant `EVENTID_FACTOR_UPDATED_THROTTLE`
of the `MathReceiver` component base class.
On each iteration, it calls `setFactor`.
At the end of this loop, the `FACTOR_UPDATED` event should be
throttled.
Next the test calls `setFactor` with a second argument of
`ThrottleState::THROTTLED`.
This code checks that the event is throttled.
Next the test sends the command `CLEAR_EVENT_THROTTLE`,
checks for the corresponding notification event,
and checks that the throttling is cleared.
<a name="The-MathReceiver-Component_Reference-Implementation"></a>
### 5.6. Reference Implementation
A reference implementation for this section is available at
`docs/Tutorials/MathComponent/MathReceiver`.
<a name="The-MathReceiver-Component_Exercises"></a>
### 5.7. Exercises
<a name="The-MathReceiver-Component_Exercises_Adding-Telemetry"></a>
#### 5.7.1. Adding Telemetry
Add a telemetry channel that records the number of math
operations performed.
1. Add the channel to the FPP model.
1. In the component implementation class, add a member
variable `numMathOps` of type `U32`.
Initialize the variable to zero in the class constructor.
1. Revise the `mathOpIn` handler so that it increments
`numMathOps` and emits the updated value as telemetry.
1. Revise the unit tests to cover the new behavior.
<a name="The-MathReceiver-Component_Exercises_Error-Handling"></a>
#### 5.7.2. Error Handling
Think about what will happen if the floating-point
math operation performed by `MathReceiver` causes an error.
For example, suppose that `mathOpIn` is invoked with `op = DIV`
and `val2 = 0.0`.
What will happen?
As currently designed and implemented, the `MathReceiver`
component will perform the requested operation.
On some systems the result will be `INF` (floating-point infinity).
In this case, the result will be sent back to `MathSender`
and reported in the usual way.
On other systems, the hardware could issue a floating-point exception.
Suppose you wanted to handle the case of division by zero
explicitly.
How would you change the design?
Here are some questions to think about:
1. How would you check for division by zero?
Note that `val2 = 0.0` is not the only case in which a division
by zero error can occur.
It can also occur for very small values of `val2`.
1. Should the error be caught in `MathSender` or `MathReceiver`?
1. Suppose the design says that `MathSender` catches the error,
and so never sends requests to `MathReceiver` to divide by zero.
What if anything should `MathReceiver` do if it receives
a divide by zero request?
Carry out the operation normally?
Emit a warning?
Fail a FSW assertion?
1. If the error is caught by `MathReceiver`, does the
interface between the components have to change?
If so, how?
What should `MathSender` do if `MathReceiver`
reports an error instead of a valid result?
Revise the MathSender and MathReceiver components to implement your
ideas.
Add unit tests covering the new behavior.
<a name="Updating-the-Ref-Deployment"></a>
## 6. Updating the Ref Deployment
The next step in the tutorial is to define instances of the
`MathSender` and `MathReceiver` components and add them
to the `Ref` topology.
<a name="Updating-the-Ref-Deployment_Defining-the-Component-Instances"></a>
### 6.1. Defining the Component Instances
Go to the directory `Ref/Top` and open the file `instances.fpp`.
This file defines the instances used in the topology for the
`Ref` application.
Update this file as described below.
**Define the mathSender instance:**
At the end of the section entitled "Active component instances,"
add the following lines:
```fpp
instance mathSender: Ref.MathSender base id 0xE00 \
queue size Default.queueSize \
stack size Default.stackSize \
priority 100
```
This code defines an instance `mathSender` of component
`MathSender`.
It has **base identifier** 0xE00.
FPP adds the base identifier to each the relative identifier
defined in the component to compute the corresponding
identifier for the instance.
For example, component `MathSender` has a telemetry channel
`MathOp` with identifier 1, so instance `mathSender`
has a command `MathOp` with identifier 0xE01.
The following lines define the queue size, stack size,
and thread priority for the active component.
Here we give `mathSender` the default queue size
and stack size and a priority of 100.
**Define the mathReceiver instance:**
At the end of the section "Queued component instances,"
add the following lines:
```fpp
instance mathReceiver: Ref.MathReceiver base id 0x2700 \
queue size Default.queueSize
```
This code defines an instance `mathReceiver` of
component `MathReceiver`.
It has base identifier 0x2700 and the default queue size.
**More information:**
For more information on defining component instances,
see
[_The FPP User's Guide_](https://fprime-community.github.io/fpp/fpp-users-guide.html#Defining-Component-Instances).
<a name="Updating-the-Ref-Deployment_Updating-the-Topology"></a>
### 6.2. Updating the Topology
Go to the directory `Ref/Top` and open the file `topology.fpp`.
This file defines the topology for the `Ref` application.
Update this file as described below.
**Add the new instances:**
You should see a list of instances, each of which begins
with the keyword `instance`.
After the line `instance linuxTime`, add the following
lines:
```fpp
instance mathSender
instance mathReceiver
```
These lines add the `mathSender` and `mathReceiver`
instances to the topology.
**Check for unconnected ports:**
Run the following commands:
```bash
fprime-util fpp-check -u unconnected.txt
cat unconnected.txt
```
You should see a list of ports
that are unconnected in the `Ref` topology.
Those ports will include the ports for the new instances
`mathSender` and `mathReceiver`.
**Connect mathReceiver to rate group 1:**
Find the line that starts `connections RateGroups`.
This is the beginning of the definition of the `RateGroups`
connection graph.
Inside the block of that definition,
find the line
`rateGroup1Comp.RateGroupMemberOut[3] -> fileDownlink.Run`.
After that line, add the line
```fpp
rateGroup1Comp.RateGroupMemberOut[4] -> mathReceiver.schedIn
```
This line adds the connection that drives the `schedIn`
port of the `mathReceiver` component instance.
**Re-run the check for unconnected ports:**
When this capability exists, you will be able to see
that `mathReceiver.schedIn` is now connected
(it no longer appears in the list).
**Add the Math connections:**
Find the Uplink connections that begin with the line
`connections Uplink`.
After the block of that definition, add the following
lines:
```fpp
connections Math {
mathSender.mathOpOut -> mathReceiver.mathOpIn
mathReceiver.mathResultOut -> mathSender.mathResultIn
}
```
These lines add the connections between the `mathSender`
and `mathReceiver` instances.
**Re-run the check for unconnected ports:**
When this capability exists, you will be able to see
that the `mathSender` and `mathReceiver` ports are connected.
**More information:**
For more information on defining topologies,
see
[_The FPP User's Guide_](https://fprime-community.github.io/fpp/fpp-users-guide.html#Defining-Topologies).
<a name="Updating-the-Ref-Deployment_Building-the-Ref-Deployment"></a>
### 6.3. Building the Ref Deployment
Go to the `Ref` directory.
Run `fprime-util build --jobs 4`.
The updated deployment should build without errors.
The generated files are located at
`Ref/build-fprime-automatic-native/Ref/Top`.
<a name="Updating-the-Ref-Deployment_Visualizing-the-Ref-Topology"></a>
### 6.4. Visualizing the Ref Topology
Now we will see how to create a visualization (graphical rendering)
of the Ref topology.
**Generate the layout:**
For this step, we will use the F Prime Layout (FPL) tool.
If FPL is not installed on your system, then install it how:
clone [this repository](https://github.com/fprime-community/fprime-layout)
and follow the instructions.
In directory `Ref/Top`, run the following commands in an sh-compatible
shell such as bash.
If you are using a different shell, you can run `sh`
to enter the `sh` shell, run these commands, and enter
`exit` when done.
Or you can stay in your preferred shell and adjust these commands
appropriately.
```bash
cp ../build-fprime-automatic-native/Ref/Top/RefTopologyAppAi.xml .
mkdir visual
cd visual
fpl-extract-xml < ../RefTopologyAppAi.xml
mkdir Ref
for file in `ls *.xml`
do
echo "laying out $file"
base=`basename $file .xml`
fpl-convert-xml $file | fpl-layout > Ref/$base.json
done
```
This step extracts the connection graphs from the topology XML and
converts each one to a JSON layout file.
**Render the layout:**
For this step, we will use the F Prime Visualizer (FPV) tool.
If FPV is not installed on your system, then install it now:
clone [this repository](https://github.com/fprime-community/fprime-visual)
and follow the instructions.
In directory `Ref/Top`, run the following commands in an sh-compatible
shell.
Replace `[path to fpv root]` with the path to the
root of the FPV repo on your system.
```bash
echo DATA_FOLDER=Ref/ > .fpv-env
nodemon [path to fpv root]/server/index.js ./.fpv-env
```
You should see the FPV server application start up on the
console.
Now open a browser and navigate to `http://localhost:3000`.
You should see a Topology menu at the top of the window
and a rendering of the Command topology below.
Select Math from the topology menu.
You should see a rendering of the Math topology.
It should look similar to the
<a href="#math-top">topology diagram shown above</a>.
You can use the menu to view other topology graphs.
When you are done, close the browser window and
type control-C in the console to shut down the FPV server.
<a name="Updating-the-Ref-Deployment_Reference-Implementation"></a>
### 6.5. Reference Implementation
A reference implementation for this section is available at
`docs/Tutorials/MathComponent/Top`.
To build this implementation, copy the files
`instances.fpp` and `topology.fpp` from
that directory to `Ref/Top`.
<a name="Running-the-Ref-Deployment"></a>
## 7. Running the Ref Deployment
Now we will use the F Prime Ground Data System (GDS) to run the Ref deployment.
Go to the `Ref` directory and run `fprime-gds`.
You should see some activity on the console.
The system is starting the Ref deployment executable, starting the GDS,
and connecting them over the local network on your machine.
After several seconds, a browser window should appear.
<a name="Running-the-Ref-Deployment_Sending-a-Command"></a>
### 7.1. Sending a Command
At the top of the window are several buttons, each of which corresponds to
a GDS view.
Select the Commanding button (this is the view that is selected
when you first start the GDS).
In the Mnemonic menu, start typing `mathSender.DO_MATH` in the text box.
As you type, the GDS will filter the menu selections.
When only one choice remains, stop typing and press return.
You should see three boxes appear:
1. A text box for entering `val1`.
1. A menu for entering `op`.
1. A text box for entering `val2`.
Fill in the arguments corresponding to the operation `1 + 2`.
You can use the tab key to move between the boxes.
When you have done this, click the Send Command button.
You should see a table entry at the bottom of the window
indicating that the command was sent.
<a name="Running-the-Ref-Deployment_Checking-Events"></a>
### 7.2. Checking Events
Now click on the Events button at the top of the window.
The view changes to the Events tab.
You should see events indicating that the command you sent was
dispatched, received, and completed.
You should also see events indicating that `mathReceiver`
performed an `ADD` operation and `mathSender`
received a result of 3.0.
<a name="Running-the-Ref-Deployment_Checking-Telemetry"></a>
### 7.3. Checking Telemetry
Click on the Channels button at the top of the window.
You should see a table of telemetry channels.
Each row corresponds to the latest value of a telemetry
channel received by the GDS.
You should see the channels corresponding to the input
values, the operation, and the result.
<a name="Running-the-Ref-Deployment_Setting-Parameters"></a>
### 7.4. Setting Parameters
Go back to the Commanding tab.
Select the command `mathReceiver.FACTOR_PRM_SET`.
This is an auto-generated command for setting the
parameter `FACTOR`.
Type the value 2.0 in the `val` box and click Send Command.
Check the events to see that the command was dispatched
and executed.
You should also see the events sent by the code
that you implemented.
In the Commanding tab, issue the command `1 + 2` again.
Check the Events tab.
Because the factor is now 2.0, you should see a result
value of 6.0.
<a name="Running-the-Ref-Deployment_Saving-Parameters"></a>
### 7.5. Saving Parameters
When you set a parameter by command, the new parameter
value resides in the component that receives the command.
At this point, if you stop and restart FSW, the parameter
will return to its original value (the value before you
sent the command).
At some point you may wish to update parameters more permanently.
You can do this by saving them to non-volatile storage.
For the Ref application, "non-volatile storage" means the
file system on your machine.
To save the parameter `mathReceiver.FACTOR` to non-volatile storage,
do the following:
1. Send the command `mathReceiver.FACTOR_PRM_SAVE`.
This command saves the parameter value to the **parameter database**,
which is a standard F Prime component for storing system parameters.
1. Send the command `prmDb.PRM_SAVE_FILE`.
This command saves the parameter values in the parameter database
to non-volatile storage.
Note that saving parameters is a two-step process.
The first step copies a single parameter from a component
to the database.
The second step saves all parameters in the database
to the disk.
If you do only the first step, the parameter will not be
saved to the disk.
<a name="Running-the-Ref-Deployment_GDS-Logs"></a>
### 7.6. GDS Logs
As it runs, the GDS writes a log into a subdirectory of `Ref/logs`.
The subdirectory is stamped with the current date.
Go into the directory for the run you just performed.
(If the GDS is still running, you will have to do this in a
different shell.)
You should see the following logs, among others:
* `Ref.log`: FSW console output.
* `command.log`: Commands sent.
* `event.log`: Event reports received.
* `channel.log`: Telemetry points received.
You can also view these logs via the GDS browser interface.
Click the Logs tab to go the Logs view.
Select the log you wish to inspect from the drop-down menu.
By default, there is no log selected.