Hello World 2
Introduction
In the previous tutorial, you worked with the MessagePrinter Component Type,
and learned how to configure it to print a message you chose at build time.
This helped you understand the various stages involved in building flight
software with Flightkit, but was not a very realistic development task.
Typically when using Flightkit to produce your flight software you will need to create or modify Component Types and write their implementations.
In this tutorial you will learn how to do that. You will add new functionality
to the MessagePrinter Component Type, implement it, and then test it in a
Deployment Type.
Along the way, you will learn how to create Actions which take Arguments. You will also be introduced to Parameters, which are used to give access to, and control over, a Component Instance's State.
|
In this tutorial, you will:
|
Before You Begin
Guidance on the styles and conventions used in these tutorials is available here.
Unless otherwise directed, all commands in this tutorial must be run in the the_basics/HelloWorld2 workspace.
You must have Flightkit installed as described in the Getting Started Guide.
Navigate to the the_basics/HelloWorld2 workspace in a terminal and run hfk workspace prepare to ensure it is ready to use.
Actions With Arguments
In the previous tutorial, you were briefly introduced to Actions as a way to execute a Component Instance's functionality on command.
Actions can also have an Argument. Like the arguments of a function in C, an Action's Argument can be used to dynamically control its behaviour.
|
In this section of the tutorial you will add a new |
Adding the printUserMessage Action
To add a new Action to a Component Type, you need to add a new <Action>
element to its componentType.xml. To specify that an Action takes an
Argument, you also need to include an <Argument> element.
Open the MessagePrinter componentType.xml, found here:
library/component_types/MessagePrinter/componentType.xml
Add the following <Action> element below the printGreeting Action:
<Action name="printUserMessage">
<Description>
Print a message, provided as an argument, to the console.
</Description>
<Argument name="message">
<Description>
The message to print.
</Description>
<Type>
<Blob fixed="false" maxLength="127"/>
</Type>
</Argument>
</Action>
Having changed the componentType.xml, you now need to generate the
Component Type.
hfk component-type generate MessagePrinter
Since an implementation file already exists for the MessagePrinter
Component Type, the above command creates a template file. This
file contains skeleton code for the Component Type's updated implementation
and can be used to update the original file. The template file is named
MessagePrinter.template.c and can be found here:
library/component_types/MessagePrinter/MessagePrinter.template.c
To update the original implementation file, you can simply copy over the new functions that are needed from the generated template file. It is recommended that you retain the same function order in the original file as is in the template file to help with comparisons and maintenance.
Copy the skeleton code for MessagePrinter_printUserMessage() from
MessagePrinter.template.c (including its description banner) and paste it
below the MessagePrinter_printGreeting() function in MessagePrinter.c.
Notice that, unlike the MessagePrinter_printGreeting() function, the new
Action function has a pu8_Message pointer argument. This pointer can be used
to access the Argument data provided by the user. The u8_Length argument
contains the length of the data pointed to by pu8_Message, and must be
honoured when accessing the pointer.
Accessing pu8_Message safely is assisted by some generated template code
which checks u8_Length against limits derived from the model. These limits
are defined in the generated include/type/MessagePrinter_ActionSrc.h header
found in the MessagePrinter directory.
In this case, the generated code is validating that u8_Length does not exceed
the maximum length of the message Argument specified in the XML excerpt you
just added.
|
Many different functions and symbols are generated from the data entered into the model for a Component Type. We demonstrate some of these symbols in these tutorials. In future we are planning to provide more complete documentation of all the generated functions and symbols which your Component Types can use. |
You must now update the implementation so it takes the byte buffer
pu8_Message and prints it out, taking into account u8_Length.
You can try this yourself if you wish, or if you’d prefer you can use the implementation we’ve provided below.
Show the modifications to the MessagePrinter_printUserMessage() Action skeleton
/*---------------------------------------------------------------------------*
* Print a message, provided as an argument, to the console.
*---------------------------------------------------------------------------*/
status_t MessagePrinter_printUserMessage
(
MessagePrinter_t *pt_MessagePrinter,
const uint8_t *pu8_Message,
uint8_t u8_Length
)
{
status_t t_Status; /* The current status */
/* Check the length of the argument is valid */
if (u8_Length > MESSAGEPRINTER_PRINT_USER_MESSAGE_MAX_ARG_SIZE)
{
/* Error: invalid argument length */
t_Status = STATUS_INVALID_PARAM;
}
else
{
TASK_PROTECTED_START(
&pt_MessagePrinter->t_Lock, >_LockTimeout, t_Status)
{
- /* TODO: Implement printUserMessage action handler */
- t_Status = STATUS_NOT_IMPLEMENTED;
+ /* An array to hold pu8_Message with a null terminator added */
+ char rc_MessageNullTerminated[
+ MESSAGEPRINTER_PRINT_USER_MESSAGE_MAX_ARG_SIZE + 1];
+
+ strncpy(
+ &rc_MessageNullTerminated[0],
+ (const char *)pu8_Message,
+ u8_Length);
+
+ /* Guarantee a null terminator */
+ rc_MessageNullTerminated[u8_Length] = '\0';
+
+ MESSAGEPRINTER_CONTAINER_LOG_INFO(
+ pt_MessagePrinter,
+ "message says '%s'",
+ &rc_MessageNullTerminated[0]);
}
TASK_PROTECTED_END(
&pt_MessagePrinter->t_Lock, >_LockTimeout, t_Status);
}
return t_Status;
}
The MessagePrinter Component Type now has a new Action printUserMessage.
Testing the printUserMessage Action
Like in the HelloWorld1 tutorial, MessagePrinter has already been
instantiated for you in a Deployment Type, and that Deployment Type has
been instantiated in a Mission.
In order to invoke the printUserMessage Action, you must build the
HelloWorld2 Mission, and run the Default Deployment Instance.
Use the same commands as in the previous tutorial to build the Mission and run the Deployment Instance:
hfk mission build HelloWorld2
./output/missions/HelloWorld2/MessagePrinterDemo/Default/Default
The Deployment Instance should start successfully:
INF: Deployment.c:298 Running deployment:
INF: Deployment.c:299 - Deployment instance: Default
INF: Deployment.c:300 - Deployment type: MessagePrinterDemo
INF: Deployment.c:301 - Target: MessagePrinterDemo
INF: Deployment.c:302 - Mission: HelloWorld2
INF: Deployment.c:427 Deployment initialisation successful
With the Deployment Instance running, you can now interact with it via Lab.
Use the same approach as in the previous tutorial to load the MDB and connect to the running Deployment Instance:
-
Launch Lab from the application menu.
-
Load the MDB using the Add mission database button on the 'Mission & Config' view.
-
Connect to the Deployment Instance by selecting your Asset from the list, clicking Connect, and entering the relevant connection settings.
Once connected, open the 'Commanding' view, and use the 'Commanding Explorer'
to find the printUserMessage Action associated with the MyMessagePrinter
Component Instance in your Deployment Instance.
Click on the printUserMessage Action to add a command card representing
that Action to the 'My Commands' section of the view.
Click on the printUserMessage command card in the 'My Commands' section to
expand its layout.
The expanded command card contains a field for entering the Argument. The details of the Action's Argument were captured in the MDB and loaded by Lab. Lab automatically provides the user interface necessary to provide the new Argument data.
You can use this field to specify the message you want the Component Instance to print to the terminal, when this Action is invoked.
Notice that there is an 'H' indicator at the right side of the field. This specifies that the argument must be entered in hexadecimal format.
Type the string that you want to be printed to the terminal, in hexadecimal format, into the Argument field, then click Invoke.
String to hex converters which are readily available online can be used to help with this step.
Show example argument
To print the word HelloWorld2, input its hexadecimal equivalent which is
48656c6c6f576f726c6432.
If successful, the terminal running your Default Deployment Instance should
now display your message:
INF: MessagePrinter.c:131 MyMessagePrinter: message says 'HelloWorld2'
Once you are happy the Action is working, disconnect Lab from the Deployment Instance, and terminate the process using the same approach as in the previous tutorial.
Keep Lab open so you can use it again later in the tutorial.
|
In this section you added an Action which takes an
Argument - In the next section you will be introduced to Parameters. You will
create one which interacts with the |
Parameters
Parameters are one of the most widely used features of Component Types created with Flightkit. Almost every Component Type has at least one Parameter to give insight into, or control over, its behaviour:
-
Many Component Types have an
enabledParameter to allow them to be switched on and off. -
Component Types which handle data transmission and reception may have Parameters for counting the number of bytes transmitted and received.
-
A Component Type for controlling a payload could represent the payload’s settings using Parameters - an imager might have Parameters for image size, resolution, filtering, and so on.
Like variables in typed programming languages, Parameters have types. Parameters may be integers, floats, bitfields, raw byte blobs. They may also be lists of integers, floats or raw byte blobs.
Parameters are either mutable or read-only. The value of Mutable Parameters can be set externally to the Component Type, whereas the value of Read-Only Parameters can only be set internally by the Component Implementation.
|
In this section of the tutorial, you will add a Read-Only Parameter indicating
how many times the This will require the addition of a new Parameter to the Component Type, the
addition of a new State variable to the Component Type Structure, and
modification of the |
Adding the actionCounter Parameter
To add a new Parameter to a Component Type, you need to add a new
<Parameter> element to its componentType.xml. MessagePrinter currently
has no Parameters, so a <Parameters> element is also required.
Open the MessagePrinter componentType.xml and add the following
<Parameters> element just below the <!-- Parameters --> comment:
<Parameters>
<Parameter name="actionCounter" readOnly="true">
<Description>
The number of times the action has been invoked.
</Description>
<Type>
<Integer signed="false" bits="32"/>
</Type>
</Parameter>
</Parameters>
As before, this changes the Component Type, so you must generate
MessagePrinter:
hfk component-type generate MessagePrinter
The above command overwrites the template file MessagePrinter.template.c with
updated code including a new Parameter getter function,
MessagePrinter_getActionCounter().
Copy this function, including its description banner, and paste it in
MessagePrinter.c just below the MessagePrinter_printUserMessage()
function.
You will implement the getter function later, but first we need to take a closer look at some of the details of Component Implementations.
The MessagePrinter_t Structure
Notice how the first argument to MessagePrinter_getActionCounter() is a
pointer to a MessagePrinter_t structure. This structure represents the
component instance whose actionCounter Parameter is being
accessed.
The MessagePrinter_t type is defined in
library/component_types/MessagePrinter/include/MessagePrinter.h.
|
While this tutorial deals only with the Every Component Type has a main header file which defines its
Component Type Structure. This header file always has the same
name as the Component Type itself with |
You can freely change the definition of the MessagePrinter_t structure to
meet the needs of the Component Implementation.
At runtime, a MessagePrinter_t structure will be declared and initialised for
each Component Instance.
In general, if the instances of your Component Type need to have some State,
you will add variables to the definition of the Component Type Structure - in
this case MessagePrinter_t.
A common pattern in Flightkit is for Parameters to access State held in the Component Type Structure.
|
You will now use this pattern to implement the |
Adding State to MessagePrinter
You need each instance of MessagePrinter to have its own counter, so a new
member of MessagePrinter_t is required.
|
It is good practice to ensure the new member’s name and type match those
defined for the Parameter in the |
Open the MessagePrinter.h header and add u32_ActionCounter to the
MessagePrinter_t definition.
Show the added MessagePrinter_t member
/** The MessagePrinter component */
typedef struct
{
/** A protection lock to protect the MessagePrinter state */
Task_ProtectionLock_t t_Lock;
- /* TODO: Add component state variables here */
+ /** Action counter */
+ uint32_t u32_ActionCounter;
}
MessagePrinter_t;
When a data member is added like this, it should always be initialised in the Component Type's local initialisation function. When a Deployment Instance is started, this function is called for each Component Instance in the Deployment Type.
This function is part of the Component Implementation so, in the case of
MessagePrinter, is found in MessagePrinter.c and is named
MessagePrinter_localInit().
Modify MessagePrinter_localInit() to provide a sensible initial value
for u32_ActionCounter.
Show the modified MessagePrinter_localInit() function
/*---------------------------------------------------------------------------*
* Carry out local component initialisation for the MessagePrinter component
*---------------------------------------------------------------------------*/
status_t MessagePrinter_localInit
(
MessagePrinter_t *pt_MessagePrinter,
const MessagePrinter_Init_t *pt_InitData
)
{
/* Use gt_LockTimeout so the component builds as is without warnings */
(void)gt_LockTimeout;
+ /* The action hasn't been invoked yet */
+ pt_MessagePrinter->u32_ActionCounter = 0;
+
/* Initialise the protection lock */
return Task_ProtectionLock_init(
&pt_MessagePrinter->t_Lock,
MESSAGEPRINTER_CONTAINER_TASKING_TYPE(pt_MessagePrinter));
}
Using Locks
Note that you need to access u32_ActionCounter in 3 separate contexts:
-
MessagePrinter_localInit()to initialise it. This only runs once at startup, so you don’t need to consider it further here. -
MessagePrinter_getActionCounter()to return its value to the user. -
MessagePrinter_printUserMessage()to increment it.
In general, there is nothing to stop the actionCounter Parameter and
printUserMessage Action from being accessed concurrently. For example, the
Action could be scheduled for autonomous invocation at the same time as a user
happens to request the Parameter's value.
To prevent potentially dangerous race conditions, Component Implementations must use Locks to protect resources which may be accessed from different contexts.
Flightkit assists you with this by providing convenient macros for creating protected sections.
Look at the MessagePrinter_getActionCounter() skeleton code you
copied from the template file earlier.
You will see it uses the TASK_PROTECTED_START() and TASK_PROTECTED_END()
macros. Using these macros creates a scope in which the Lock object passed into
the macros is held, and cannot be acquired by any other context.
As long as both MessagePrinter_getActionCounter() and
MessagePrinter_printUserMessage() use locked sections to access
u32_ActionCounter, there is no possibility of race conditions.
|
Practically, the chance of race conditions in this case is extremely small, but not negligible! It is particularly important to think carefully about concurrent access when working with Flightkit, because your code may be run in many different environments with wildly different low-level behaviour. Finally, while the protected resource in this tutorial is very simple (a single 32-bit word), there are many situations, like the handling of packets in communications Component Types, where a failure to correctly protect underlying resources can cause very difficult to diagnose issues when the software is integrated. |
Implementing the actionCounter Parameter
You now have all the elements needed to update the implementation such that
actionCounter behaves as specified in the componentType.xml:
-
An underlying State variable held in the Component Type Structure named
u32_ActionCounter, and -
An understanding of how to use Locks to protect this State variable.
You will now update the implementation using these elements.
First, modify the MessagePrinter_getActionCounter() Parameter getter
function you added earlier so it returns the value of u32_ActionCounter to
the user, accessing it from within the locked section.
Show the updated MessagePrinter_getActionCounter() implementation
/*---------------------------------------------------------------------------*
* Get the actionCounter parameter
*---------------------------------------------------------------------------*/
status_t MessagePrinter_getActionCounter
(
MessagePrinter_t *pt_MessagePrinter,
uint32_t *pu32_Value
)
{
status_t t_Status; /* The current status */
TASK_PROTECTED_START(&pt_MessagePrinter->t_Lock, >_LockTimeout, t_Status)
{
- /* TODO: Implement actionCounter get accessor */
- t_Status = STATUS_NOT_IMPLEMENTED;
+ *pu32_Value = pt_MessagePrinter->u32_ActionCounter;
}
TASK_PROTECTED_END(&pt_MessagePrinter->t_Lock, >_LockTimeout, t_Status);
return t_Status;
}
Next, update the MessagePrinter_printUserMessage() function so that,
each time the Action is invoked, it increments the u32_ActionCounter State
variable. Again, this must be performed inside the function’s locked section.
Show the change required in the MessagePrinter_printUserMessage() implementation
/* Guarantee a null terminator */
rc_MessageNullTerminated[u8_Length] = '\0';
MESSAGEPRINTER_CONTAINER_LOG_INFO(
pt_MessagePrinter,
"message says '%s'",
&rc_MessageNullTerminated[0]);
+
+ pt_MessagePrinter->u32_ActionCounter++;
}
TASK_PROTECTED_END(
&pt_MessagePrinter->t_Lock, >_LockTimeout, t_Status);
}
return t_Status;
}
Testing the actionCounter Parameter
You have now completed the MessagePrinter implementation.
In order to test the behaviour of actionCounter you must once again
build the HelloWorld2 Mission and run the Default
Deployment Instance.
hfk mission build HelloWorld2
./output/missions/HelloWorld2/MessagePrinterDemo/Default/Default
Return to your active Lab window.
Load the MDB again using the same steps as earlier to include the latest changes you made to the Component Type.
Connect to your running Deployment Instance using the same steps as before.
Locate the printUserMessage Action and the 'actionCounter' Parameter
using the 'Commanding Explorer', and add command cards for each of them to the
'My Commands' section.
Click on the actionCounter command card to expand its layout.
Issue a command to get the current value of the actionCounter Parameter
by clicking its Get ⬇ button.
A value of 0 should be returned because that is the Parameter's initial
value, as set in MessagePrinter_localInit().
Invoke the printUserMessage Action by expanding its command card, entering
a string argument of your choice in hexadecimal format, then clicking the
Invoke button.
Issue another get command on the actionCounter Parameter.
The returned value should have incremented by one since the last time it was
issued as the printUserMessage Action has been invoked once since then.
Wrap Up
In this tutorial you have:
-
Added to a Component Type's implementation using the same steps that will be required when working on your own Component Types in the future.
-
Extended your understanding of Actions to include Arguments, and learned how they can be used to provide input to your Component Types.
-
Learned about Parameters and how they can be implemented using your Component Type Structure.
-
Been introduced to Locks and their role in preventing race conditions arising from multiple access to resources.
In the next tutorial you will be introduced to more capabilities you can add to Component Types. This will build on the understanding you have developed here using a different Component Type and Deployment Type.
Click here to move on to that now.