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:

  • Get more comfortable with the workflow involved in writing Component Implementations.

  • Learn how to create Actions which take Arguments.

  • Learn about Parameters, and how they can be implemented by adding to the Component Type Structure.

  • Learn how to use Locks to protect resources from multiple access.

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 printUserMessage Action. Rather than printing a message defined in the MessagePrinter initialisation data, this Action will print the message you provide as an Argument.

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, &gt_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, &gt_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 - printUserMessage - to the MessagePrinter Component Type. You have implemented this Action such that it prints its Argument.

In the next section you will be introduced to Parameters. You will create one which interacts with the printUserMessage Action to give insight into the behaviour of MessagePrinter Component Instances at runtime.

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 enabled Parameter 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 printUserMessage Action has been invoked.

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 printUserMessage Action.

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 MessagePrinter Component Type, these ideas apply to all Component Types.

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 .h appended.

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 actionCounter Parameter. Note that while this is a common way of implementing Parameters, it is not mandatory. In principle Parameter values can be derived in any way which is convenient.

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 componentType.xml. This makes your Component Implementation easier to understand for others.

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, &gt_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, &gt_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, &gt_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.