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:
|
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
readandwrite. 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
sendandreceive. 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
openFileandreadFile. 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 |
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
captureImageAction. 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
loadSettingsAction, 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 |
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
loadSettingsAction must first open the file indicated bysettingsPath. The FSSopenFileOperation can achieve this. -
It then needs to read the settings data from the open file. The FSS
readFileOperation 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
closeFileOperation can be used for this.
|
If you are confident working from this outline, you can try and implement the
|
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, >_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, >_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, >_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, >_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, >_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
Note that some output is always printed, regardless of the value of |
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
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.