Hardware Interaction 2: PS

Introduction

In this tutorial we introduce the Packet Service (PS), and show how it is used in Flightkit to exchange data between Component Instances.

We also introduce Modules, and explain how they can be used to provide additional functionality to a Component Type.

To demonstrate these concepts you will develop a Component Type to run on a Raspberry Pi Zero W which receives data from an attached GPS device using PS.

While this tutorial produces a Component Type for running on a Raspberry Pi Zero W, the ideas and techniques introduced are useful for developing Component Types which interact with subsystems on any of the platforms which Flightkit supports.

For simplicity, whenever we reference the Raspberry Pi Zero W in this tutorial, we will refer to it as the "Pi".

In this tutorial, you will:

  • Learn when and how to use the Packet Service (PS).

  • Learn about Modules, and how they can be used by Component Types.

  • Gain a broader understanding of Services.

  • Gain a broader understanding of how to interact with subsystems.

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/HardwareInteraction2 workspace.

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

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

If you haven’t already, follow the steps in our Raspberry Pi Zero How-to Guide to set up your Pi and install the correct toolchain in order to complete this tutorial.

Packet Service (PS)

As discussed in the previous tutorial, subsystems on spacecraft are often connected to centralised onboard computers (OBCs). Different subsysytems will specify different interaction patterns for use when communicating with them.

In addition to the "request-response" pattern you’ve already learned about, some subsystems will need to communicate with each other in a peer-to-peer fashion.

In this interaction pattern each peer has the same communication abilities, including the ability to initiate communication. For this reason, all peers on the network should be prepared to receive unprompted data.

PS is a Service which represents the ability to interact in this way.

A PS Provider promises to send data to, and listen for data from, a given subsystem when a connected PS Consumer requests it.

PS therefore defines the following Operations:

  • send - Transmit data to a given subsystem

  • receive - Receive data from a given subsystem

  • acknowledge - Transmit an acknowledge packet to a given subsystem

Analysis and Design

As stated in the previous tutorial, when developing Component Types to provide some form of hardware interaction, it is important to first identify each piece of hardware involved and understand how they operate.

In this scenario we wish to develop a means of interacting with a GPS device, so that specific location data can be accessed on request. GPS devices tend to provide location data unprompted, so you’ll make use of PS to receive this data.

In this tutorial we can describe the Pi as the "receiver" and the GPS device as the "sender". You’ll consider each of these in turn.

In more general peer-to-peer communication, parties to the communication will need to act as both "sender" and "receiver". Achieving this using Flightkit requires very similar code to that which you will write in this tutorial, so we won’t complicate matters here by covering that case as well!

The Sender

The sender which the flight software needs to receive data from is the M5Stack Mini GPS/BDS Unit, which uses the AT6558 satellite navigation SOC chip. This chip supports a variety of different satellite navigation systems, producing accurate location data with an error of less than 2 metres. To find out more about this chip you can refer to its datasheet, available online here.

Although the chip itself provides support for various serial interfaces, the M5Stack Mini GPS/BDS Unit only exposes the UART interface. It is this interface that you will use to receive data from the chip.

The M5Stack Mini GPS/BDS Unit is designed to be "plug and play". Once connected, the AT6558 chip automatically starts interpreting satellite signals, and calculating its own location. It then transmits this location data in the form of National Marine Electronics Association (NMEA) sentences via its UART interface.

In order to make use of the location data from the sender, you will need to produce a Component Type capable of parsing the NMEA sentences and extracting location information from them.

The Receiver

The receiver in this scenario is the Pi’s on board PL011 UART. You can find out more about how this receiver works by referring to its datasheet, available online here. The PL011 UART is more than capable of serving as a receiver for the data from the GPS device, as we require.

We have provided a Serial Component Type for interacting with the PL011 in our FlightkitFoundation bundle. You will use this Component Type to receive the data from the GPS device and pass it to the Component Type described above.

The Serial Component Type Provides PS and uses the Service to receive (and send) data via the UART. As such, the Component Type you will need to create for parsing the GPS location data will need to consume PS.

With the sender and receiver hardware understood, you can move on to implementing the new Component Type you’ve identified.

The AT6558 Component Type

In the previous section it was determined that a Component Type will need to be created for handling the location data generated by the AT6558 chip in the M5Stack Mini GPS/BDS Unit.

As stated in the previous tutorial, when creating Component Types for interacting with specific subsystems, it is recommended to name the type after the device it interacts with, as opposed to the subsystem the device is packaged in, as this encourages reuse. For this reason we will name the new Component Type AT6558.

Definition

In this section you will define enough of the AT6558 Component Type to make a start on its implementation.

Using PS is more complicated than the MAS service requirement you worked with in the previous tutorial, so some iteration between the componentType.xml and the implementation in C will be required.

This iteration between XML and C is typical of the Flightkit development process. It is unusual to correctly describe in XML every aspect of your implementation on the first attempt!

As usual, to produce a new Component Type you must first create a componentType.xml file on disk.

Create the Component Type using the following command:

hfk component-type create AT6558

Open the created componentType.xml file, found in library/component_types/AT6558

Update the description and documentation elements to give more detail about what the Component Type will do:

<Description>
  This component provides an interface to the AT6558 satellite navigation SOC chip.
</Description>
<Documentation><Markdown>
  Datasheet available from:

  https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/docs/datasheet/unit/AT6558_en.pdf

  This component is capable of accessing the following GPS data:

  * Latitude
  * Longitude
</Markdown></Documentation>

As discussed above it was determined that the new AT6558 Component Type will need to consume PS to receive the location data from the Serial Component Type, and hence the GPS device itself.

To consume PS you will need to add the following <Services> element below the <!-- Services (Provided, Required, Subscribed, Brokered and Published)--> comment:

<Services>
  <Required><Service name="bus" type="io.PS">
    <Description>
      Used to access the serial bus the AT6558 chip is connected to.
    </Description>
  </Service></Required>
</Services>

This defines a PS requirement named bus which, when AT6558 is instantiated, must be connected up to a PS Provider.

By default this Service connection will be Synchronous. This means that the Operations issued by the AT6558 will block until they’re finished. Particularly in the case of the PS receive operation, this is undesirable, as you have no way of knowing how long you may have to wait for a GPS message to arrive.

Fortunately, you can change a Service requirement to be Asynchronous. This changes the generated Service functions which your Component Implementation can call in three important ways:

  1. The functions no longer block.

  2. The functions work with Transaction structures which hold the state of an ongoing Service Operation.

  3. The functions require you to associate a Sporadic Task with each Transaction. The task is used to process each transaction when it is completed.

To change a Service requirement from Synchronous to Asynchronous you need to make 2 specific additions to the componentType.xml.

First, add the following <Implementation> element below the <!-- Implementation --> comment to make the bus Service requirement Asynchronous:

<Implementation connectionPhase="true">
  <Description>
    Connection phase used for initiating asynchronous receives.
  </Description>
  <Service name="bus" defaultAccess="async"/>
</Implementation>

By setting connectionPhase="true", you are specifying that the AT6558 Component Type requires an additional initialisation step which occurs after all Component Instances in a Deployment Type have been locally initialised. Your implementation will use this step to issue Asynchronous PS receive Operations to the connected PS Provider.

Next, add the following <Tasks> element below the <!-- Tasks --> comment, and define a sporadic task named receive for processing received GPS data:

<Tasks>
  <SporadicTask name="receive" defaultQueueSize="10">
    <Description>
      This task processes GPS data received from the AT6558 chip.
    </Description>
  </SporadicTask>
</Tasks>

In the Component Implementation you will associate the receive Task with the PS transactions. When the PS Provider completes the transactions, this Task will be triggered to parse the data received from the GPS device.

The Task's defaultQueueSize is set to 10. This allows up to 10 transactions of received data to be queued up while the Component Instance is busy processing data.

Your AT6558 componentType.xml now contains everything it needs in order to receive data from the GPS device. Your next step will be to add a mechanism which can be used to access the location information it has parsed.

As you have seen in earlier tutorials, this is typically achieved in Flightkit using Parameters.

Add the following <Parameters> element defining the latitude and longitude Parameters below the <!-- Parameters --> comment:

<Parameters>
  <Parameter name="latitude" readOnly="true">
    <Description>
      The most recent latitude reading, in degrees.
    </Description>
    <Type>
      <Real bits="32"/>
    </Type>
  </Parameter>
  <Parameter name="longitude" readOnly="true">
    <Description>
      The most recent longitude reading, in degrees.
    </Description>
    <Type>
      <Real bits="32"/>
    </Type>
  </Parameter>
</Parameters>

The Parameters should be read-only because they are presenting data received from the GPS device. It does not make sense to set the latitude or longitude.

Your Component Type is now ready to be generated.

Generate the AT6558 Component Type using the following command:

hfk component-type generate AT6558

With the Component Type generated you can now move onto its implementation.

Using the PS requirement

In this section you will use the bus PS Service requirement, which you added to the componentType.xml above, to receive GPS data.

Recall that above you set the bus requirement to be Asynchronous. In order to make use of an Asynchronous PS Service requirement, you first need to declare some Transactions which the AT6558 Component Type will use when instantiated.

Two arrays are required - one for holding PS Transaction objects, and another for declaring the buffer space which will be associated with each Transaction object.

Add an array of PS Transactions and an array of uint8_t buffers to hold received data to the AT6558_t Component Type Structure in AT6558.h:

Show the state variables added to the Component Type Structure
/** The AT6558 component */
typedef struct
{
    /** Initialisation data */
    const AT6558_Init_t *pt_Init;
    /** A protection lock to protect the AT6558 state */
    Task_ProtectionLock_t t_Lock;
-   /* TODO: Add component state variables here */
+   /** The PS transactions for receiving messages */
+   PS_Transaction_t rt_RxTransactions[AT6558_CONFIG_RX_TRXS];
+   /** The buffers for the PS transactions */
+   uint8_t rru8_Buffers[AT6558_CONFIG_RX_TRXS][AT6558_CONFIG_TRX_BUFFER_LEN];
}
AT6558_t;

In order to use the PS_Transaction_t data type you also need to include the header file containing its definition.

Add the following header file to the list of #include directives at the top of AT6558.h:

Show the added #include
#include "types.h"
#include "status.h"

#include "type/AT6558_ParamSrc.h"
#include "AT6558_config.h"
#include "Task_ProtectionLock.h"
+#include "PS_Transaction.h"

Notice that in declaring the rt_RxTransactions[] and rru8_Buffers[][] arrays you have made use of two symbols to control their sizes: AT6558_CONFIG_RX_TRXS and AT6558_CONFIG_TRX_BUFFER_LEN. These symbols control how many Transactions are available to receive data into, and how many bytes each Transaction can hold.

While it is not required, it is often useful for the end-user of your Component Type to be able to control how much memory it uses when instantiated. The number of transactions may also impact how quickly the Component Type can receive data from the GPS device. For these reasons we recommend you define these symbols in the AT6558_config.h header. This will allow end-users to override their values at build time.

Add definitions for the symbols described above to AT6558_config.h:

Show the added configuration symbols
/*---------------------------------------------------------------------------*
 * Build configuration defines
 *---------------------------------------------------------------------------*/

#ifndef AT6558REF_CONFIG_LOCK_TIMEOUT
/** The timeout to use for lock operations, in microseconds. */
#define AT6558REF_CONFIG_LOCK_TIMEOUT                       500000
#endif
+
+#ifndef AT6558_CONFIG_RX_TRXS
+/** How many transactions to use for receiving messages */
+#define AT6558_CONFIG_RX_TRXS                              10
+#endif
+
+#ifndef AT6558_CONFIG_TRX_BUFFER_LEN
+/** The size of the buffer for each transaction */
+#define AT6558_CONFIG_TRX_BUFFER_LEN                       100
+#endif

As described above, the values chosen for these symbols will affect memory usage and performance. It is difficult to provide general guidance on trade-offs like this, but the rule-of-thumb we have followed here is:

  • Allocate the same number of transactions as the defaultQueueSize of the Task which will process them.

  • Allocate the same number of bytes as you typically expect to arrive in quick succession. This usually matches some application-specific frame, packet or message size.

With this in mind we have suggested values of 10 and 100 above, respectively.

Your Component Type Structure now contains the members required to receive data using the PS Service requirement.

As with any state variable, you must make sure to initialise these members at start up within the Component Type's local initialisation function, AT6558_localInit().

Call PS_Transaction_init() for each transaction in the rt_RxTransactions[] array in AT6558_localInit().

Show the changes to AT6558_localInit()
status_t AT6558_localInit
(
    AT6558_t *pt_AT6558,
    const AT6558_Init_t *pt_InitData
)
{
    status_t t_Status;          /* Current status */

    /*
     * Initialise the component structure
     */

    /* Set the initialisation data */
    pt_AT6558->pt_Init = pt_InitData;

-   /* TODO: Carry out basic component initialisation */
-
-   /* Basic component initialisation was successful */
+   /* No errors yet */
    t_Status = STATUS_SUCCESS;

    /* Only continue if initialisation so far was successful */
    if (t_Status == STATUS_SUCCESS)
    {
        /* Initialise the protection lock */
        t_Status = Task_ProtectionLock_init(
            &pt_AT6558->t_Lock,
            AT6558REF_CONTAINER_TASKING_TYPE(pt_AT6558));
    }
+
+   /* Initialise each receive transaction */
+   for (uint32_t u32_Count = 0;
+        ((u32_Count < ARRAY_COUNT(pt_AT6558->rt_RxTransactions)) &&
+         (t_Status == STATUS_SUCCESS));
+        u32_Count++)
+   {
+       t_Status = PS_Transaction_init(
+           &pt_AT6558->rt_RxTransactions[u32_Count],
+           AT6558_CONTAINER_TASKING_TYPE(pt_AT6558),
+           AT6558_CONTAINER_TASK_QUEUE_RECEIVE(pt_AT6558),
+           &gt_LockTimeout);
+   }

    return t_Status;
}

Notice the third argument to PS_Transaction_init(). The AT6558_CONTAINER_TASK_QUEUE_RECEIVE() macro yields a pointer to the receive task queue. By passing the receive task queue to PS_Transaction_init() you are specifying that the Transaction should be placed on that queue when it is completed. When the Transaction is placed on the queue the Task will be triggered and its task function, AT6558_taskReceive(), will be called. This is where you will later add code to parse and process the received GPS data.

With the Transactions initialised they can be used for receiving or sending data. The AT6558 only needs to receive data from the GPS device, and you will implement that now.

receive Operations are issued by calling AT6558_PS_receiveAsyncBus(). The first place you need to call this function is in the AT6558_connectionInit() function.

Recall that this function is called as part of the initialisation process after all Component Instances in the Deployment Instance have been locally initialised. This is the earliest point when Service Operations can be issued, and doing so ensures instances of your Component Type are ready to receive GPS data as soon as possible after startup.

In addition to calling AT6558_PS_receiveAsyncBus() to issue the receive, you will also need to associate one of the receive buffers you declared in rru8_Buffers[][] with each Transaction you are using.

Add calls to set the receive buffer and then issue the asynchronous PS receive operation for each transaction in the AT6558_connectionInit() function:

Show the updated AT6558_connectionInit() implementation
status_t AT6558_connectionInit
(
    AT6558_t *pt_AT6558
)
{
    status_t t_Status;          /* Current status */

-   /* Basic component initialisation was successful */
+   /* No errors yet */
    t_Status = STATUS_SUCCESS;
+
+   /* For each receive transaction */
+   for (uint32_t u32_Count = 0;
+        ((u32_Count < ARRAY_COUNT(pt_AT6558->rt_RxTransactions)) &&
+         (t_Status == STATUS_SUCCESS));
+        u32_Count++)
+   {
+       /* Set up the receive buffer */
+       t_Status = PS_Transaction_setReceiveBuffer(
+           &pt_AT6558->rt_RxTransactions[u32_Count],
+           &pt_AT6558->rru8_Buffers[u32_Count][0],
+           AT6558_CONFIG_TRX_BUFFER_LEN);
+
+       if (t_Status == STATUS_SUCCESS)
+       {
+           /* Queue the asynchronous receive */
+           t_Status = AT6558_PS_receiveAsyncBus(
+               pt_AT6558,
+               &pt_AT6558->rt_RxTransactions[u32_Count],
+               NULL);
+       }
+   }

    return t_Status;
}

Your Component Implementation will now carry out its initialisation such that received GPS data will be passed to its receive Task. With this done, you must complete the PS Transaction handling by adding some code to the Task such that Transactions will be processed and reused. Without this code in the Task, your Component Type will run out of transactions to use for reception.

In order to process received Transactions you must:

  1. Retrieve the associated data buffer from the Transaction with PS_Transaction_getReceiveBuffer().

  2. Check whether the completion was successful with PS_Transaction_getCompletionStatus(). If the completion status is not STATUS_SUCCESS then you must assume the receive buffer contains no valid data.

  3. If the completion status is STATUS_SUCCESS then you should parse the data. You will do this in the next section - for now leave a TODO.

  4. Regardless of the result of receiving and parsing the data, you must reset the Transaction using PS_Transaction_reset().

  5. You then need to call PS_Transaction_setReceiveBuffer() and AT6558_PS_receiveAsyncBus() as you did in AT6558_connectionInit(). By doing this in the receive task, you ensure that your Component Implementation can process received data on an ongoing basis by recycling the Transactions it uses.

This is a general process which you can use whenever you need to process received PS Transactions in a Sporadic Task.

Add the calls described above to the receive Task function, AT6558_taskReceive().

Show the receive task
void AT6558_taskReceive
(
    AT6558_t *pt_AT6558,
    void *pv_QueueContext
)
{
-   /* TODO: Insert task code here */
+   /* Check we have a non-NULL context. If we don't, then there's nothing to
+    * do. */
+   if (pv_QueueContext != NULL)
+   {
+       status_t t_Status;
+       PS_Transaction_t *pt_Transaction;
+       uint8_t *pu8_Buffer;
+       uint32_t u32_Size;
+
+       /* Extract the transaction from the context */
+       pt_Transaction = (PS_Transaction_t *)pv_QueueContext;
+
+       /* Get the receive buffer and size from the transaction */
+       t_Status = PS_Transaction_getReceiveBuffer(
+           pt_Transaction,
+           &pu8_Buffer,
+           &u32_Size);
+
+       /* If no errors */
+       if (t_Status == STATUS_SUCCESS)
+       {
+           status_t t_CompletionStatus;
+
+           /* Check the completion status */
+           t_Status = PS_Transaction_getCompletionStatus(
+               pt_Transaction,
+               &t_CompletionStatus);
+
+           /* If no errors */
+           if (t_Status == STATUS_SUCCESS)
+           {
+               /* Set status to the completion status */
+               t_Status = t_CompletionStatus;
+           }
+
+           /* If no errors */
+           if (t_Status == STATUS_SUCCESS)
+           {
+               TASK_PROTECTED_START(
+                   &pt_AT6558->t_Lock,
+                   &gt_LockTimeout,
+                   t_Status)
+               {
+                   /* TODO Parse the received data in pu8_Buffer */
+               }
+               TASK_PROTECTED_END(
+                   &pt_AT6558->t_Lock,
+                   &gt_LockTimeout,
+                   t_Status);
+           }
+
+           /* Reset the PS transaction regardless of success or not */
+           t_Status = PS_Transaction_reset(pt_Transaction);
+
+           /* If no errors */
+           if (t_Status == STATUS_SUCCESS)
+           {
+               /* Set up the receive buffer */
+               t_Status = PS_Transaction_setReceiveBuffer(
+                   pt_Transaction,
+                   pu8_Buffer,
+                   AT6558_CONFIG_TRX_BUFFER_LEN);
+           }
+
+           /* If no errors */
+           if (t_Status == STATUS_SUCCESS)
+           {
+               /* Queue the asynchronous receive */
+               t_Status = AT6558_PS_receiveAsyncBus(
+                   pt_AT6558,
+                   pt_Transaction,
+                   NULL);
+           }
+       }
+   }
}

The AT6558 Component Type will now correctly handle received Transactions using its receive task. In combination with the work you did earlier on initialisation, it is now a functional PS Consumer to which application-specific code can be added.

Using PS asynchronously is more complicated than the MAS Service requirement you worked with in the previous tutorial, so a recap of the above implementation steps may be helpful.

Briefly, you implemented Asynchronous PS Service requirement by:

  1. Declaring an array, rt_RxTransactions[], of PS transaction in the Component Type Structure.

  2. Declaring an array, rru8_Buffers[][], of buffers in the Component Type Structure.

  3. Calling PS_Transaction_init() in AT6558_localInit() to associate the receive task with each Transaction.

  4. Calling PS_Transaction_setReceiveBuffer() in AT6558_connectionInit() to associate a valid data buffer with each Transaction.

  5. Calling AT6558_PS_receiveAsyncBus() in AT6558_connectionInit() to start a receive with each available Transaction.

  6. Calling PS_Transaction_getReceiveBuffer() and PS_Transaction_getCompletionStatus() in AT6558_taskReceive() to determine whether valid data was received.

  7. Calling PS_Transaction_reset() in AT6558_taskReceive() to make the Transaction ready for reuse.

  8. Calling PS_Transaction_setReceiveBuffer() and AT6558_PS_receiveAsyncBus() in AT6558_taskReceive() so that the Transaction can be used by the PS Provider to receive more data.

You can now complete the implementation of the AT6558 Component Type by adding parsing code and implementing the Parameter getters.

Completing the Implementation

As described above, the AT6558 Component Type will receive NMEA sentences from the connected M5Stack Mini GPS/BDS Unit. The format of these sentences is standardised and not specific to this GPS device. As a result, it would be useful to split the functionality for parsing NMEA sentences from the specific Component Type you are working on so that it can be reused by other Component Types in future. One way this can be achieved in Flightkit is by producing a Module.

Modules are buildable artefacts that contain type definitions, exceptions, symbols, macros and functions which Component Types can use to aid with their implementation. As is the case with all Flightkit artefacts, Modules are defined in an XML file, and implemented in C.

Implementing a Module to provide NMEA parsing functionality from scratch would be quite time consuming, so in the interest of keeping this tutorial short we have provided one for you.

Open library/modules/NMEA/module.xml.

This file is used to define the Module. It specifies the Module's name, and includes a brief description of what it contains. Other elements can be added to provide further documentation, define exceptions and require other Modules.

As you can see, this Module provides functionality for interacting with NMEA sentences, and requires use of functionality defined within the algorithms.String Module.

Open library/modules/NMEA/include/NMEA.h.

This file defines symbols and declares type definitions and functions which are publicly accessible via the Module. It includes a function for extracting GGA sentences from NMEA sentences, a function for extracting latitude and longitude values from GGA sentences, and a type for keeping track of the parsing state. It also includes a symbol which defines the maximum length of a GGA sentence.

Open library/modules/NMEA/NMEA.c.

This file implements the functions declared within NMEA.h. Both functions are implemented using a similar state machine approach that iterates through the different fields within a sentence, and extracts and validates the relevant data.

Now that you have a good understanding of what the NMEA Module contains, you can make use of it to help implement the remainder of your receive task function.

In order to enable your Component Type to make use of the functionality contained within the NMEA Module, you must update its definition to require that Module, then generate it for the changes to take effect.

Add the following XML element below the <!-- Required Modules and Components --> comment in the componentType.xml.

<Required>
  <Modules>
    <Module name="NMEA"/>
  </Modules>
</Required>

Generate the AT6558 Component Type.

With the Module required, you can now use the functions contained within it to complete the TODO in your AT6558_taskReceive() function. The goal is to replace the TODO with code that:

  1. Extracts a GGA sentence from the received NMEA data.

  2. Extracts the latitude and longitude values from the GGA sentence.

  3. Stores the latitude and longitude values in component state so they can be retrieved on demand.

The functions within the NMEA Module require a variety of arguments to track the parsing state and store the extracted data. The values of these arguments will need to be preserved between successive task executions for the following reasons:

  1. NMEA sentences may be split between multiple transactions, so multiple task executions might be required to parse one sentence.

  2. Latitude and longitude values will need to be stored external to the task context so that they can be retrieved on demand, independent of when the task runs.

You can create new members in the Component Type Structure to preserve these values.

Add the following members to the AT6558_t Component Type Structure in AT6558.h to preserve the NMEA parsing state:

Show the added members.
/** The AT6558 component */
typedef struct
{
    /** Initialisation data */
    const AT6558_Init_t *pt_Init;
    /** A protection lock to protect the AT6558 state */
    Task_ProtectionLock_t t_Lock;
    /** The PS transactions for receiving messages */
    PS_Transaction_t rt_RxTransactions[AT6558_CONFIG_RX_TRXS];
    /** The buffers for the PS transactions */
    uint8_t rru8_Buffers[AT6558_CONFIG_RX_TRXS][AT6558_CONFIG_TRX_BUFFER_LEN];
+   /** The current state of the parsing state machine */
+   NMEA_ParsingState_t t_ParsingState;
+   /** A buffer for storing the most recently received GGA sentence */
+   char rc_GgaBuffer[NMEA_GGA_MAX_SENTENCE_SIZE];
+   /** The number of bytes contained in the GGA buffer */
+   uint32_t u32_GgaByteCount;
+   /** The computed checksum value for the data in the GGA buffer */
+   uint8_t u8_GgaChecksum;
+   /** A flag indicating if the most recently received GGA sentence is valid */
+   bool b_GgaValid;
+   /** Most recent latitude value */
+   float32_t f32_Latitude;
+   /** A flag indicating if the most recent latitude value is valid */
+   bool b_LatitudeValid;
+   /** Most recent longitude value */
+   float32_t f32_Longitude;
+   /** A flag indicating if the most recent longitude value is valid */
+   bool b_LongitudeValid;
}
AT6558_t;

Notice that the t_ParsingState and rc_GgaBuffer[] member variables in the provided code make use of types and symbols defined within the NMEA Module, NMEA_ParsingState_t and NMEA_GGA_MAX_SENTENCE_SIZE. To make use of these within this file, the Module's header file needs to be added to the list of include directives.

Add the NMEA Module's header file to the list of include directives at the top of AT6558.h.

Show the updated list of include directives in AT6558.h
#include "types.h"
#include "status.h"

#include "type/AT6558_ParamSrc.h"
#include "AT6558_config.h"
+#include "NMEA.h"
#include "Task_ProtectionLock.h"
#include "PS_Transaction.h"

As usual, the new member variables need to be initialised in the Component Type's AT6658_localInit() function.

Initialise the new member variables' values in AT6558_localInit():

Show the changes to AT6558_localInit()
status_t AT6558_localInit
(
    AT6558_t *pt_AT6558,
    const AT6558_Init_t *pt_InitData
)
{
    status_t t_Status;          /* Current status */

    /*
     * Initialise the component structure
     */

    /* Set the initialisation data */
    pt_AT6558->pt_Init = pt_InitData;

    /* No errors yet */
    t_Status = STATUS_SUCCESS;
+
+   /* Initialise state variables */
+   pt_AT6558->t_ParsingState = NMEA_PARSING_STATE_START;
+   pt_AT6558->u32_GgaByteCount = 0;
+   pt_AT6558->u8_GgaChecksum = 0;
+   pt_AT6558->b_GgaValid = false;
+   pt_AT6558->f32_Latitude = 0;
+   pt_AT6558->b_LatitudeValid = false;
+   pt_AT6558->f32_Longitude = 0;
+   pt_AT6558->b_LongitudeValid = false;

    /* Only continue if initialisation so far was successful */
    if (t_Status == STATUS_SUCCESS)
    {
        /* Initialise the protection lock */
        t_Status = Task_ProtectionLock_init(
            &pt_AT6558->t_Lock,
            AT6558_CONTAINER_TASKING_TYPE(pt_AT6558));
    }

    /* If no errors, for each receive transactions */
    for (uint32_t u32_Count = 0;
         ((u32_Count < ARRAY_COUNT(pt_AT6558->rt_RxTransactions)) &&
          (t_Status == STATUS_SUCCESS));
         u32_Count++)
    {
        /* Initialise the receive transaction */
        t_Status = PS_Transaction_init(
            &pt_AT6558->rt_RxTransactions[u32_Count],
            AT6558_CONTAINER_TASKING_TYPE(pt_AT6558),
            AT6558_CONTAINER_TASK_QUEUE_RECEIVE(pt_AT6558),
            &gt_LockTimeout);
    }

    return t_Status;
}

With all member variables declared and initialised you can now update AT6558_taskReceive() to implement the logic described earlier, in place of the TODO comment.

Replace the TODO in AT6558_taskReceive() with code that implements the logic described earlier, using the functions defined within the NMEA Module:

Show the updated receive task
                TASK_PROTECTED_START(
                    &pt_AT6558->t_Lock,
                    &gt_LockTimeout,
                    t_Status)
                {
-                   /* TODO Parse the received data in pu8_Buffer */
+                   status_t t_ParseStatus;
+
+                   /* Extract a GGA sentence from the NMEA data */
+                   t_ParseStatus = NMEA_extractGga(
+                       pu8_Buffer,
+                       u32_Size,
+                       &pt_AT6558->t_ParsingState,
+                       pt_AT6558->rc_GgaBuffer,
+                       ARRAY_COUNT(pt_AT6558->rc_GgaBuffer),
+                       &pt_AT6558->u32_GgaByteCount,
+                       &pt_AT6558->u8_GgaChecksum,
+                       &pt_AT6558->b_GgaValid);
+
+                   /* If no errors have occurred */
+                   if (t_ParseStatus == STATUS_SUCCESS)
+                   {
+                       /* If a valid GGA sentence has been extracted */
+                       if (pt_AT6558->b_GgaValid)
+                       {
+                           /* Extract the latitude and longitude values from
+                            * the GGA sentence */
+                           NMEA_extractLatLon(
+                               pt_AT6558->rc_GgaBuffer,
+                               ARRAY_COUNT(pt_AT6558->rc_GgaBuffer),
+                               &pt_AT6558->f32_Latitude,
+                               &pt_AT6558->b_LatitudeValid,
+                               &pt_AT6558->f32_Longitude,
+                               &pt_AT6558->b_LongitudeValid);
+                       }
+                   }
+                   else
+                   {
+                       AT6558_CONTAINER_LOG_ERROR(
+                           pt_AT6558,
+                           "An unexpected error was raised when parsing " \
+                           "NMEA data (%" PRIu32 ")",
+                           t_ParseStatus);
+                   }
                }
                TASK_PROTECTED_END(
                    &pt_AT6558->t_Lock,
                    &gt_LockTimeout,
                    t_Status);

That completes the receive Task implementation. The next step is to implement the Component Type's Parameter accessor functions.

Update AT6558_getLatitude() so that it returns the f32_Latitude value stored in the Component Type's state, so long as it is a valid value:

Show the updated AT6558_getLatitude() function
status_t AT6558_getLatitude
(
    AT6558_t *pt_AT6558,
    float32_t *pf32_Value
)
{
    status_t t_Status;          /* The current status */

    TASK_PROTECTED_START(&pt_AT6558->t_Lock, &gt_LockTimeout, t_Status)
    {
-       /* TODO: Implement latitude get accessor */
-       t_Status = STATUS_NOT_IMPLEMENTED;
+       /* If latitude value is valid */
+       if (pt_AT6558->b_LatitudeValid)
+       {
+           /* Return value */
+           *pf32_Value = pt_AT6558->f32_Latitude;
+       }
+       else
+       {
+           /* Update status */
+           t_Status = STATUS_INVALID_PARAM;
+       }
    }
    TASK_PROTECTED_END(&pt_AT6558->t_Lock, &gt_LockTimeout, t_Status);

    return t_Status;
}

Update AT6558_getLongitude() so that it returns the f32_Longitude value stored in the Component Type's state, so long as it is a valid value:

Show the updated AT6558_getLongitude() function
status_t AT6558_getLongitude
(
    AT6558_t *pt_AT6558,
    float32_t *pf32_Value
)
{
    status_t t_Status;          /* The current status */

    TASK_PROTECTED_START(&pt_AT6558->t_Lock, &gt_LockTimeout, t_Status)
    {
-       /* TODO: Implement longitude get accessor */
-       t_Status = STATUS_NOT_IMPLEMENTED;
+       /* If longitude value is valid */
+       if (pt_AT6558->b_LongitudeValid)
+       {
+           /* Return value */
+           *pf32_Value = pt_AT6558->f32_Longitude;
+       }
+       else
+       {
+           /* Update status */
+           t_Status = STATUS_INVALID_PARAM;
+       }
    }
    TASK_PROTECTED_END(&pt_AT6558->t_Lock, &gt_LockTimeout, t_Status);

    return t_Status;
}

And finally, we need to address the lingering TODO comment in AT6558.h which refers to the Initialisation Data structure. This Component Type does not require any Initialisation Data, so replace it with a comment to that effect.

Replace the TODO comment in the AT6558_Init_t data structure with a comment stating that no Initialisation Data is required.

That completes the implementation for the AT6558 Component Type.

The GPSDemo Deployment Type

With the AT6558 Component Type complete, it can now be instantiated in a Deployment Type, and that Deployment Type instantiated in a Mission, to test out the new functionality.

As in the previous tutorial, we’ve provided a Deployment Type and Mission which you’ll now update in order to see your AT6558 Component Type in action.

New Component Instances

Open the deploymentType.xml file found in library/deployment_types/GPSDemo.

Add the following two Component Instances to the bottom of the <Implementation> element:

Show the Component Instances added to the deploymentType.xml file.
      <Component name="comms.services.PASTarget" type="core.component.pas.PASTarget"/>
      <Component name="comms.services.PASMessaging" type="io.ams.AMSTargetPS">
        <Connections>
          <Services>
            <Service name="initiator" component="comms.SpacePacket" service="dataPacket" channel="0" />
            <Service name="serviceTarget" component="comms.services.PASTarget" service="remote" />
          </Services>
        </Connections>
        <Tasks>
          <SporadicTask name="receive" priority="0" />
          <SporadicTask name="waitForReplyComplete" priority="0" />
          <SporadicTask name="sendCompleteServiceTarget" priority="0" />
          <SporadicTask name="requestCompleteServiceTarget" priority="0" />
        </Tasks>
      </Component>
+     <Component name="UART" type="drivers.linux.io.bus.Serial">
+       <Description>
+         Platform's PL011 UART which interfaces with the serial bus.
+       </Description>
+       <Tasks>
+         <InterruptTask name="receive" priority="0"/>
+         <InterruptTask name="transmit" priority="0"/>
+         <SporadicTask name="characterTimeout" priority="0"/>
+         <SporadicTask name="resetDevice" priority="0"/>
+         <SporadicTask name="receiveTimeout" priority="0"/>
+         <SporadicTask name="transmitTimeout" priority="0"/>
+       </Tasks>
+     </Component>
+     <Component name="M5GPS" type="AT6558">
+       <Description>
+         Peripheral for receiving satellite navigation data.
+       </Description>
+       <Connections>
+         <Services>
+           <Service name="bus" component="UART" service="data"/>
+         </Services>
+       </Connections>
+       <Tasks>
+         <SporadicTask name="receive" priority="0"/>
+       </Tasks>
+     </Component>
    </Implementation>
  </DeploymentType>
</ModelElement>

You can find out what the Serial Component Type, or any other Component Type you are using from a bundle, requires in order to be successfully instantiated by generating documentation for it:

hfk component-type generate-docs drivers.linux.io.bus.Serial

This will generate documentation in the output directory.

Note that by connecting the Component Instances up as shown, the M5GPS instance’s PS Service requirement has been fulfilled by the UART instance’s PS Service provision. This means that the UART instance will handle all requests made by the M5GPS instance to receive data from the serial bus.

Now that you have updated the Deployment Type, you can generate the Mission in which it is instantiated. This will verify that you have connected the various Component Instances in the Deployment Type correctly.

Generate the HardwareInteraction2 Mission using following command:

hfk mission generate HardwareInteraction2

This produces a Mission Database which will allow you to interact with the Deployment Instance via Lab. It also produces empty Initialisation Data for the new Component Instances which you will populate later.

Setting the Initialisation Data

As usual, before building the Mission, you must set the Initialisation Data for the new Component Instances in their respective Deployment Instances.

Open missions/HardwareInteraction2/GPSDemo/Default/init/UART_Init.c.

Here you must specify the Initialisation Data for the UART Component Instance as per the Serial_Init_t data structure defined in Serial.h.

The first member to set is z_Device. Since you will be using UART0 on the hardware, set this to the value "/dev/serial0". This is the alias that is internally mapped to UART0 on the Pi.

The next member to set is u32_BaudRate. According to the documentation, the M5Stack Mini GPS/BDS Unit operates with a default baud rate of 9600. In order to communicate with it you must configure the UART to use the same rate. For this reason set the member value to 9600.

Next is b_IgnoreStartupFault. This specifies whether to ignore errors during initialisation. We want errors to be reported so set this to false.

The final member is b_ParityBit. This specifies whether to operate the UART in parity mode or not. Again, the documentation for the M5Stack Mini GPS/BDS Unit states that it does not use parity by default, so set it to false.

Set the Initialisation Data for the UART Component Instance using the explanation provided above.

Show updated Initialisation Data
/** The UART initialisation data */
const Serial_Init_t gt_UARTInit =
    {
-       /* TODO: Add initialisation data for the UART component */
+       /** The path to the device file */
+       .z_Device = "/dev/serial0",
+       /** The desired baud rate */
+       .u32_BaudRate = 9600,
+       /** Ignore failures to open the device file during component
+        * initialisation */
+       .b_IgnoreStartupFault = false,
+       /** Parity bit is added */
+       .b_ParityBit = false
    };

Now set the Initialisation Data for the M5GPS Component Instance.

Open missions/HardwareInteraction2/GPSDemo/Default/init/M5GPS_Init.c.

According to the AT6558_Init_t data structure defined in AT6558.h, no Initialisation Data is required.

Update the comment in the Initialisation Data structure to state that no Initialisation Data is required.

With the Initialisation Data complete, you can now move on to testing your flight software on the target hardware.

Testing the GPS Device

In order to build and run Flightkit flight software on your Pi you will need to follow the steps in our Raspberry Pi Zero How-to Guide!

The steps below will fail if your machine or the Pi are not correctly configured.

Prior to running anything on the platform, ensure that the peripheral is properly connected to the OBC as per the Initialisation Data configured.

If indoors, position the hardware next to a window so that it can acquire a GPS fix more easily.

Build HardwareInteraction2 using the following command:

hfk mission build HardwareInteraction2

This builds a binary for the Deployment Instance that can now be run on the Pi. The binary is found here:

output/missions/HardwareInteraction2/GPSDemo/Default/Default

To run this binary on the Pi, you first need to copy it over to the device.

Copy the binary to the platform using the following command and entering your password when prompted.

If you’ve changed your username or hostname from the default values, be sure to use your own.

scp output/missions/HardwareInteraction2/GPSDemo/Default/Default pi@raspberrypi:

With the binary on the Pi filesystem, you can now log in to the Pi and run it.

Using another terminal, SSH into the platform using the following command, again ensuring to use your own username and hostname if required:

ssh pi@raspberrypi

Once connected, you can run the binary like you would any other executable.

Using the same terminal, run the binary using the following command:

./Default

If successful, the following output should be printed to the terminal:

INF: Deployment.c:342 Running deployment:
INF: Deployment.c:343 - Deployment instance: Default
INF: Deployment.c:344 - Deployment type:     GPSDemo
INF: Deployment.c:345 - Target:              GPSDemo
INF: Deployment.c:346 - Mission:             HardwareInteraction2
INF: Deployment.c:471 Deployment initialisation successful

With the Deployment Instance running on the platform, you can now connect to it using Lab and test out the new functionality.

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

Once connected, use the 'Commanding' view to get the current values of both the latitude and longitude Parameters.

Verify that the latitude and longitude values are correct by comparing them with your current known GPS location.

If your get requests are returning error code 2, indicating an invalid parameter, it is likely that the GPS device does not have a valid fix.

If this occurs, try positioning the GPS device in a location with a clearer view of the sky, and allow it 5 minutes to acquire a fix before re-running the test.

Congratulations, you have now used PS to successfully interact with your second hardware subsystem.

Wrap Up

In this tutorial you have:

  • Learned how to interact with subsystems using the Packet Service (PS).

  • Learned how to configure a Component Type to make use of functionality contained within Modules.

  • Gained a deeper understanding of Services and how they can be used to connect Component Instances together.

  • Gained a deeper understanding of the workflow used when developing component types which interact with subsystems.

What Now?

This concludes the Flightkit tutorials on Component Type development. Hopefully you now have a good grasp of the fundamentals of building Component Types with Flightkit.

If you have not already reviewed the guides and reference material provided with Flightkit, you can do so now:

Alternatively, you can start working on your own Component Types and Missions.

If you have feedback or questions about these tutorials, please get in touch with us via our customer portal.