Using a Service

Introduction

This tutorial introduces Services as a powerful way to allow one Component Instance to make use of functionality provided by another.

In this tutorial, you will:

  • Learn about the conceptual background to Services.

  • Create a Component Type which accesses a filesystem via Service Operations.

  • Learn how to make Service connections between Component Instances in Deployment Types.

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 developing_components/UsingAService workspace.

You must have Flightkit installed as described in the Getting Started Guide.

Navigate to the developing_components/UsingAService workspace in a terminal and run hfk workspace prepare to ensure it is ready to use.

Introducing Services

Contracts

In earlier tutorials you learned that Component Types are how we encapsulate functionality for reuse in Flightkit.

In order to use Component Types to build more complex systems they need to be connected together. A Component Instance which encodes data for downlink, for example, needs to be connected to a Component Instance for transmitting data via a radio.

Services are the primary means of making these connections between Component Instances in Flightkit.

A Service is a contract between Component Types. A Component Type can promise to fulfil the obligations of the Service. It can also state that it wants access to a Service to use the functionality it represents.

In this way Component Instances can be reliably connected together, regardless of when and by whom their corresponding Component Types were developed.

Thinking of Services in contractual terms is useful because it allows you to ignore the other characteristics of the Component Instances involved. For the purposes of connecting Component Instances together with Services, all that matters is that the relevant Component Types provide or consume the right Service.

Usage Patterns

Flightkit provides two kinds of Services for making these contractual connections between Component Instances.

  • Publish-Subscribe Services, in which a Publisher Publishes a Message for eventual use by any number of Subscribers. The EDS Service you met in the Hello World 3 tutorial falls into this category.

  • Request-Response Services, in which a Consumer issues an Operation to which a Provider must respond. This tutorial focuses on this kind of Service.

While some details are specific to Flightkit, the above usage patterns are very similar to those found in RPC and messaging frameworks. One widely available example of such a framework is ZeroMQ.

A Request-Response Service's contract consists of the Operations it defines.

A Component Type which Provides a Request-Response Service must do whatever is required to implement the Operations in the Service. It must handle requests from a connected Component Instance in line with the definition of the Service.

Conversely, a Component Type which Consumes a Request-Response Service is stating that, when instantiated, it must be connected to a Provider of that Service. The Component Implementation can assume this will be true.

Services in Flightkit

There are many examples of Provision and Consumption of Request-Response Services in Flightkit's library:

  • Request-based interaction with hardware is represented by the Memory Access Service (MAS). It defines Operations like read and write. SPI and I2C drivers Provide MAS, while Component Types which access subsystems via these buses Consume it.

  • Packet data in communications stacks is passed between Component Instances using the Packet Service (PS). It defines Operations like send and receive. Radio subsystems will usually Provide PS, and Component Types which wish to send and receive data will Consume it.

  • Access to files and directories on filesystems is represented by the File Store Service (FSS). It defines Operations like openFile and readFile. Component Types which give access to a filesystem Provide FSS, while those which need to read and write file data Consume it.

You will learn more about this final example now, and you will see MAS and PS again in later tutorials.

The File Store Service

FSS is a Service consisting of Operations for accessing file systems. It is modelled after the POSIX file API.

The contract which a given Service defines is captured in the model in the form of a service.xml file. hfk can use the Service definition to produce human-readable Service documentation.

Generate the FSS Service documentation by running the following command:

hfk service generate-docs storage.FSS

Open the documentation generated under output.

As discussed above, a Service represents a contract between two Component Types. An important difference between Services and other kinds of API is that a Service is defined independently of either of the Component Types which make use of it.

Indeed, a single Service can be used as the contract between many different pairs of Component Types.

FSS defines many Operations, including those for opening files, writing to them, reading from them, and manipulating the directory structure of a filesystem.

A Provider of FSS must implement these Operations, while a Consumer may issue any of the Operations on offer, as long as it follows the rules given in the FSS service.xml.

Flightkit comes with the PosixFS FSS Provider, which you will be using later on. It implements the FSS Service Operations by making POSIX filesystem API calls. This means it can provide FSS on POSIX-compliant platforms like Linux.

In the next section you will see these concepts in action by creating an FSS Consumer and connecting it to a PosixFS Component Instance.

Implementing a Consumer

Typically you will need to implement Service Consumers more often than Providers. The strict definition of Services means you can usually make use of pre-existing Service Providers to meet the needs of new Service Consumers.

In this section you’ll work on a SimpleImager Component Type which emulates some features you might expect a real imager subsystem to have. To keep things simple, you’ll be focusing on adding functionality to allow the imager’s settings to be read in from a file.

Once this is done, you’ll test it by instantiating it within a Deployment Type, and connecting it to a PosixFS Component Instance.

The SimpleImager Component Type

Open the SimpleImager componentType.xml found here:

library/component_types/SimpleImager/componentType.xml

As you can see, the Component Type already defines the interface the operator will use:

  • It has a captureImage Action. Since this Component Type emulates an imager, invoking this Action just prints some information about the commanded image capture to the terminal.

  • It has a loadSettings Action, which, as you can see in the documentation, takes a path to a settings file as its Argument.

  • It has Parameters which, once a settings file is loaded, will allow the values from the file to be inspected by the operator.

loadSettings is the Action you will be implementing.

Adding an FSS Requirement

At the moment SimpleImager looks like Component Types found in earlier tutorials. It has Actions and Parameters and not much else!

To make use of FSS, you need to make SimpleImager an FSS Consumer. This is achieved by adding a <Services><Required><Service> element to its componentType.xml.

Add the <Services> element just below the <!-- Services (Provided, Required, Subscribed, Brokered and Published) --> comment:

<Services>
  <Required>
    <Service name="fs" type="storage.FSS">
      <Description>
        Access to the file system where SimpleImager settings files are found
      </Description>
    </Service>
  </Required>
</Services>

As usual, this addition to the componentType.xml requires the Component Type to be generated.

Generate the Component Type.

If you are not comfortable with using hfk to generate Component Types, you can revisit earlier tutorials to refresh your memory.

Designing loadSettings

Since you have added an FSS Consumption to the SimpleImager Component Type and generated it, several new files will have appeared.

The key file for issuing FSS Operations is SimpleImager_FSS_package.h. This file will have been generated at the following path:

library/component_types/SimpleImager/include/container/SimpleImager_FSS_package.h

This file contains the functions which the SimpleImager implementation must call to issue FSS Operations.

Each Component Type which requires a given Service type calls its own functions to issue Service Operations.

This is in contrast to a typical API, where all clients of an API make the same calls.

Now that your Component Implementation can issue FSS Operations, it’s time to use them to implement the loadSettings Action. The FSS service.xml and the documentation of your FSS functions in SimpleImager_FSS_package.h would help you with this, but we can jump straight in as follows:

  • The loadSettings Action must first open the file indicated by settingsPath. The FSS openFile Operation can achieve this.

  • It then needs to read the settings data from the open file. The FSS readFile Operation can achieve this.

  • The settings data then needs to be parsed. We have provided the SimpleImager_Parser_parseSettings() function to do this.

  • The file needs to be closed, regardless of any failures in processing the data. The FSS closeFile Operation can be used for this.

If you are confident working from this outline, you can try and implement the loadSettings Action yourself. If not, we will cover each of the above steps in more detail now.

Implementing loadSettings

To issue the FSS Operations defined in SimpleImager_FSS_package.h, it needs to be included in the Component Implementation.

Open the SimpleImager Component Implementation and add an #include for SimpleImager_FSS_package.h at the top.

Show the added #include directive
#include "types.h"
#include "status.h"
#include "SimpleImager.h"
#include "container/SimpleImager_Container_package.h"
#include "SimpleImager_Parser_package.h"
+#include "container/SimpleImager_FSS_package.h"
#include "Task_ProtectionLock.h"
#include "Util_Log.h"
#include <string.h>

In the loadSettings Action implementation, you will now add a call to SimpleImager_FSS_openFileFs(). Much like a POSIX fopen() call, this function takes a path to a file and returns a file handle to you.

In Flightkit, Service Operations can use Selection Identifiers to reference the resources they use. FSS Operations use Selection Identifiers as file handles.

In the SimpleImager_loadSettings() Action implementation, add a call to SimpleImager_FSS_openFileFs().

Use the documentation in SimpleImager_FSS_package.h to determine the Arguments, or refer to the snippet below.

Show the added call to SimpleImager_FSS_openFileFs()
/*---------------------------------------------------------------------------*
 * Load imager settings from the given file
 *---------------------------------------------------------------------------*/

status_t SimpleImager_loadSettings
(
    SimpleImager_t *pt_SimpleImager,
    const uint8_t *pu8_SettingsPath,
    uint8_t u8_Length
)
{
    status_t t_Status;          /* The current status */

    /* Check the length of the argument is valid */
    if ((u8_Length == 0) ||
        (u8_Length > SIMPLEIMAGER_LOAD_SETTINGS_MAX_ARG_SIZE))
    {
        /* Error: invalid argument length */
        t_Status = STATUS_INVALID_PARAM;
    }
    else
    {
        /* Make a copy of pu8_SettingsPath guaranteed to be null terminated */
        uint8_t ru8_SettingsPath[SIMPLEIMAGER_LOAD_SETTINGS_MAX_ARG_SIZE + 1];

        strncpy(
            (string_t)&ru8_SettingsPath[0],
            (cstring_t)pu8_SettingsPath,
            u8_Length);
        ru8_SettingsPath[u8_Length] = 0;

        TASK_PROTECTED_START(
            &pt_SimpleImager->t_Lock, &gt_LockTimeout, t_Status)
        {
-           /* TODO: Implement loadSettings action handler */
-           t_Status = STATUS_NOT_IMPLEMENTED;
+           Service_SelectionID_t t_SettingsFile;
+
+           /* We haven't loaded any settings yet. */
+           pt_SimpleImager->b_SettingsLoaded = false;
+
+           t_Status = SimpleImager_FSS_openFileFs(
+               pt_SimpleImager,
+               NULL,
+               (cstring_t)&ru8_SettingsPath[0],
+               false,
+               false,
+               FSS_ACCESSMODE_READ_ONLY,
+               &t_SettingsFile);
        }
        TASK_PROTECTED_END(
            &pt_SimpleImager->t_Lock, &gt_LockTimeout, t_Status);
    }

    return t_Status;
}

Notice that we must take some care to make sure the file name Argument passed to SimpleImager_FSS_openFileFs() is null-terminated.

Next, add a call to SimpleImager_FSS_readFileFs() to read SIMPLEIMAGER_PARSER_MAX_SETTINGS_BYTES bytes from the open settings file.

The SIMPLEIMAGER_PARSER_MAX_SETTINGS_BYTES symbol is defined by the parser code we have written for you in SimpleImager_Parser.c. The file format we have created does not create large files, so there should be no problem with adding a buffer on the stack to hold all of the settings data.

Show the added call to SimpleImager_FSS_readFileFs()
            /* We haven't loaded any settings yet. */
            pt_SimpleImager->b_SettingsLoaded = false;

            t_Status = SimpleImager_FSS_openFileFs(
                pt_SimpleImager,
                NULL,
                (cstring_t)&ru8_SettingsPath[0],
                false,
                false,
                FSS_ACCESSMODE_READ_ONLY,
                false,
                &t_SettingsFile);
+
+           if (t_Status == STATUS_SUCCESS)
+           {
+               uint8_t ru8_SettingsData[SIMPLEIMAGER_PARSER_MAX_SETTINGS_BYTES];
+               uint32_t u32_SettingsDataLen = ARRAY_COUNT(ru8_SettingsData);
+
+               t_Status = SimpleImager_FSS_readFileFs(
+                   pt_SimpleImager,
+                   NULL,
+                   t_SettingsFile,
+                   &ru8_SettingsData[0],
+                   &u32_SettingsDataLen,
+                   u32_SettingsDataLen);
+           }
        }
        TASK_PROTECTED_END(
            &pt_SimpleImager->t_Lock, &gt_LockTimeout, t_Status);
    }

    return t_Status;
}

Notice how SimpleImager_FSS_readFileFs() takes pu32_DataLength pointer argument. This allows the FSS Provider to indicate if less data was read than requested by modifying the value of *pu32_DataLength.

Now, add a call passing ru8_SettingsData to the provided SimpleImager_Parser_parseSettings() function.

The SimpleImager Component Type Structure already contains a structure to hold parsed settings. Pass a pointer to this structure to the parsing function.

Be sure to set the b_SettingsLoaded member of the Component Type Structure to true if parsing the settings succeeds!

Show the added call to SimpleImager_Parser_parseSettings()
            if (t_Status == STATUS_SUCCESS)
            {
                uint8_t ru8_SettingsData[SIMPLEIMAGER_PARSER_MAX_SETTINGS_BYTES];
                uint32_t u32_SettingsDataLen = ARRAY_COUNT(ru8_SettingsData);

                t_Status = SimpleImager_FSS_readFileFs(
                    pt_SimpleImager,
                    NULL,
                    t_SettingsFile,
                    &ru8_SettingsData[0],
                    &u32_SettingsDataLen,
                    u32_SettingsDataLen);
+
+               if (t_Status == STATUS_SUCCESS)
+               {
+                   t_Status = SimpleImager_Parser_parseSettings(
+                       &ru8_SettingsData[0],
+                       u32_SettingsDataLen,
+                       &pt_SimpleImager->t_Settings);
+               }
+
+               if (t_Status == STATUS_SUCCESS)
+               {
+                   SIMPLEIMAGER_CONTAINER_LOG_INFO(
+                       pt_SimpleImager,
+                       "Successfully loaded settings from '%s'!",
+                       (cstring_t)&ru8_SettingsPath[0]);
+
+                   pt_SimpleImager->b_SettingsLoaded = true;
+               }
            }
        }
        TASK_PROTECTED_END(
            &pt_SimpleImager->t_Lock, &gt_LockTimeout, t_Status);
    }

    return t_Status;
}

Finally, add a call to SimpleImager_FSS_closeFileFs(). This must be called regardless of read errors or parsing failures.

Make sure you don’t overwrite failures in the earlier steps with a failure to close the settings file.

Show the added call to SimpleImager_FSS_closeFileFs()
            if (t_Status == STATUS_SUCCESS)
            {
                uint8_t ru8_SettingsData[SIMPLEIMAGER_PARSER_MAX_SETTINGS_BYTES];
                uint32_t u32_SettingsDataLen = ARRAY_COUNT(ru8_SettingsData);
+               status_t t_CloseStatus;

                t_Status = SimpleImager_FSS_readFileFs(
                    pt_SimpleImager,
                    NULL,
                    t_SettingsFile,
                    &ru8_SettingsData[0],
                    &u32_SettingsDataLen,
                    u32_SettingsDataLen);

                if (t_Status == STATUS_SUCCESS)
                {
                    t_Status = SimpleImager_Parser_parseSettings(
                        &ru8_SettingsData[0],
                        u32_SettingsDataLen,
                        &pt_SimpleImager->t_Settings);
                }

                if (t_Status == STATUS_SUCCESS)
                {
                    SIMPLEIMAGER_CONTAINER_LOG_INFO(
                        pt_SimpleImager,
                        "Successfully loaded settings from '%s'!",
                        (cstring_t)&ru8_SettingsPath[0]);

                    pt_SimpleImager->b_SettingsLoaded = true;
                }
+
+               t_CloseStatus = SimpleImager_FSS_closeFileFs(
+                   pt_SimpleImager, NULL, t_SettingsFile);
+
+               if (t_CloseStatus != STATUS_SUCCESS)
+               {
+                   SIMPLEIMAGER_CONTAINER_LOG_ERROR(
+                       pt_SimpleImager,
+                       "Failed to close settings file '%s'",
+                       (cstring_t)&ru8_SettingsPath[0]);
+               }
            }
        }
        TASK_PROTECTED_END(
            &pt_SimpleImager->t_Lock, &gt_LockTimeout, t_Status);
    }

    return t_Status;
}

Once you’ve completed the above steps, the loadSettings Action is fully implemented. In the next section you will instantiate SimpleImager in a Deployment Type within a Mission. This will allow you to test the loadSettings Action.

The SimpleImagerDemo Deployment Type

We have provided the SimpleImagerDemo Deployment Type for you to use to test SimpleImager. There is already a SimpleImager Component Instance in the Deployment Type. The Deployment Type has been instantiated in the UsingAService Mission.

If, however, you try and generate or build the UsingAService Mission now, it will fail. This is because the new SimpleImager.fs Service Consumption you added has not been connected to an FSS Provider

Connecting fs

Open the SimpleImagerDemo deploymentType.xml and find the SimpleImager Component Instance near the bottom.

To connect SimpleImager.fs to an FSS Provider, you must add a <Connections><Services><Service> element to the SimpleImager Component Instance in SimpleImagerDemo.

Add the following <Connections> element just below the SimpleImager instance’s </Description> tag:

      <Component name="SimpleImager" type="SimpleImager">
        <Description>
          An instance of the SimpleImager component type
        </Description>
+       <Connections><Services>
+         <Service name="fs" component="core.FileSystem" service="fs" />
+       </Services></Connections>
      </Component>

This connects the SimpleImager Service Consumption specified by the name attribute to the Service Provision specified by the service attribute on the Component Instance specified by the component attribute.

In this case SimpleImager.fs will be fulfilled by core.FileSystem.fs.

Updating the Library Configuration

With SimpleImager.fs connected, the Component Instance is correctly set up and can now be tested. Before doing that, however, you will need to change the Library Configuration used to build the Deployment Instance so you can see all the debug output from the loadSettings action.

Library Configurations allow you to control the arguments passed to the C compiler when building each Component Type in a Deployment Instance. Along with Initialisation Data, Library Configurations give you control over how the Component Types and Component Instances you use will behave at runtime.

When you implemented the loadSettings Action above, you used the SIMPLEIMAGER_CONTAINER_LOG_INFO() macro to provide feedback on the console when a settings file is successfully loaded. By default, only ERROR level debug messages - those produced by SIMPLEIMAGER_CONTAINER_LOG_ERROR(), for example - are printed. Lower level messages are discarded at build time.

To see the INFO message you added, you will need to set UTIL_LOG_LEVEL to 3 (INFO) when building the SimpleImager Component Type. You’ll do this using the Library Configuration for the Default Deployment Instance in the UsingAService Mission.

Open the Library Configuration meson.build file found here:

missions/UsingAService/SimpleImagerDemo/Default/lib_config/meson.build

Define a new value for the log level by adding to the SimpleImager compiler arguments variable:

component_types_SimpleImager_c_args += '-DUTIL_LOG_LEVEL=3'

This will cause the SIMPLEIMAGER_CONTAINER_LOG_INFO() macro you added above to produce output at runtime.

You can give UTIL_LOG_LEVEL any value between 0 and 4. The meaning of each level is as follows:

  • 0 - all output disabled.

  • 1 - only ERROR output is enabled.

  • 2 - WARN and ERROR output is enabled.

  • 3 - INFO, WARN and ERROR output is enabled.

  • 4 - DEBUG, INFO, WARN and ERROR output is enabled.

Note that some output is always printed, regardless of the value of UTIL_LOG_LEVEL, such as the INFO output produced when your Deployment Instances are initialised.

Library Configurations can be used for more than just changing the log level. Often Component Types will allow their memory usage and runtime performance to be tweaked at build time. For this tutorial, however, the default configuration values for the Component Types you are using are fine.

SimpleImager can now be tested:

To test SimpleImager and its FSS Consumption, generate, build and run the Deployment Instance using the same approach as in previous tutorials:

hfk mission build UsingAService
./output/missions/UsingAService/SimpleImagerDemo/Default/Default

You should see the familiar message, Deployment initialisation successful.

Creating a Test File

Before testing the loadSettings Action, you will need some "settings" stored in a file on your machine. You will pass the path to this file to loadSettings. The code you’ve written will open the file and read the settings within.

Create a file with the correct format containing some SimpleImager settings.

The correct format is given in the documentation for SimpleImager_Parser_parseSettings() in SimpleImager_Parser_package.h. Here is an example:

T=10000;H=640;W=480;

You can use any text editor you like to create this file anywhere you like on your machine, as long as the path to it is less than 127 characters long.

This restriction on the length of the file path initially comes from the definition of the loadSettings Action in the SimpleImager componentType.xml, but there are some other restrictions as well:

  • PosixFS has a build-time configurable restriction on the length of paths it accepts. By default this is 256.

  • FSS has an internal limit of 128 characters on the length of elements in a path - that is, each string between / characters.

While these limits slightly restrict functionality on Linux systems, they allow us to support filesystems on resource-constrained platforms using the same Component Types and Services as those we use on Linux.

Testing loadSettings

You will now use Lab to test SimpleImager, in particular the loadSettings Action which you have implemented.

Open Lab, load the MDB and connect to the Deployment Instance.

Add a command card for the loadSettings Action to the My Commands section of the Commanding view.

Expand the loadSettings command card. Notice that it contains an Argument field for entering the path to the settings file that you would like to load. This argument must be entered in hexadecimal format.

Enter the absolute path to the settings file you created earlier, in hexadecimal format.

String to hex converters which are readily available online can be used to help with this step.

You can find the absolute path of your settings file in the file explorer via Right click → Properties, or by running the following command in the terminal using your own filename:

realpath my_settings.txt
Show example output
/home/example/my_settings.txt

Invoke the Action by clicking the Invoke button.

If the settings file was successfully loaded you should see output similar to:

INF: SimpleImager.c:198 SimpleImager: Successfully loaded settings from '/home/example/my_settings.txt'!

Your implementation of loadSettings took the three settings values and stored them in the t_Settings structure within the SimpleImager Component Type Structure.

You can access the members of this structure using the exposureTime, imageHeight and imageWidth Parameters. The values should match those you put in your settings file.

You can also invoke the captureImage Action which will report the settings it has in place for "capturing" an image.

This tutorial has demonstrated using FSS to open and read files. The Service is also capable of writing files, creating directories, listing directory contents, and many other useful file system operations.

In later tutorials you will learn more about other Services defined in Flightkit, including PS and MAS.

Wrap Up

In this tutorial you have:

  • Learned about the concept of a Service in Flightkit.

  • Learned the difference between Request-Response Service Consumers and Providers.

  • Made use of the generated functions which grant Consumers access to their connected Providers.

  • Learned how to connect Consumers and Providers within a Deployment Type.

In the next tutorial you will learn about how unit testing is carried out in Flightkit. This will allow you to test and maintain your Component Types more efficiently than through the ad hoc approach you’ve been relying on so far.

Click here to move on to that now.