Hardware Interaction 2: PS

Introduction

In this tutorial we introduce the Packet Service (PS), and show how it is used in Flight Software Development Kit to exchange data between component instances.

To demonstrate these concepts, you will develop a Component Type to run on a Raspberry Pi (2 & 3) which receives data from an attached GPS device using PS.

While this tutorial produces a Component Type for running on a Raspberry Pi (2 & 3), the ideas and techniques introduced are useful for developing Component Types which interact with subsystems on any of the platforms which Flight Software Development Kit supports.

For simplicity, whenever we reference the Raspberry Pi (2 & 3) 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).

  • Gain a broader understanding of Services.

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

Before You Begin

In each terminal you use during this tutorial, the GEN1_ROOT environment variable must be set. To do this, you must follow the instructions.

You must run the commands found in this tutorial from within the OBSW/Source directory.

You can switch to this directory using the following command:

$ cd $GEN1_ROOT/OBSW/Source

If this command fails, check that GEN1_ROOT is correctly set!

If you haven’t already, follow the steps in our Raspberry Pi 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 to request specific location data. 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". We will consider each of these in turn.

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 through its UART interface.

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.

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.

The Serial Component Type provides PS and uses the Service to receive (and send) data through 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 FSDK development process. It is unusual to correctly describe in XML every aspect of your implementation on the first attempt!

You can skip steps 3, 4, and 5 if you have completed the Hardware Interaction 1 tutorial.

Open a terminal in the fsdk-22.2/OBSW/Source/demo_pi directory and create a new component library using the following command:

codegen library new components

The above command generates the following directory structure within the demo_pi directory for the new library:

components
├── config
|   └── project.mk
├── inc
├── src
├── test
|   └── src
|       └── common
|           └── deployment
|               └── Deployment.c
└── Makefile

This library can now be used to hold new Component Types.

As the new Component requires PS, we need to add a dependency to the component library that defines the PS Component Type - in this case, the framework module.

The project.mk file controls how the component library is built. Update the component library's project.mk file to include the framework directory.

# Dependencies (library directories)
DEPEND_DIRS = $(OBSW_ROOT)/framework

We use OBSW_ROOT to refer to the FSDK flight software source directory. You will need to set GEN1_ROOT environment variable as shown in: Before you Begin

While we are here we should also increase our DUTIL_LOG_LEVEL to 3 to enable more verbose logging.

Update component library's DUTIL_LOG_LEVEL to 3.

# Preprocessor flags
CPPFLAGS += -DUTIL_LOG_LEVEL=3

Create the Component Type using the following command:

codegen componenttype new components --name AT6558

Open the created componentType.xml model, found in components/inc/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)--> 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 operation 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 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 Flight Software Development Kit 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>
    <Value type="float" bits="32"/>
  </Parameter>
  <Parameter name="longitude" readOnly="true">
    <Description>
      The most recent longitude reading, in degrees.
    </Description>
    <Value type="float" bits="32"/>
  </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:

codegen componenttype generate components --name 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 ui8_t buffers to hold received data to the AT6558_t component type structure in AT6558.types.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 */
+   ui8_t rru8_Buffers[AT6558_CONFIG_RX_TRXS][AT6558_CONFIG_TRX_BUFFER_LEN];
}
AT6558_t;

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_types.h header file:

Show the added #include
#include "core/types.h"
#include "core/status.h"
#include "AT6558/AT6558_sizes.h"
#include "AT6558/AT6558_config.h"
#include "task/Task_ProtectionLock.h"
+ #include "io/PS/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 have control of 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 the AT6558_config.h header file:

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

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 Task's defaultQueueSize.

  • Allocate the number of bytes you 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, respectively.

Your component type structure now contains the members required to receive data using the PS Service requirement.

You now need to initialise these members at start up within the Component Type's local initialisation function, AT6558_localInit().

We need to initalise each element within the rt_RxTransactions[] array.

In the AT6558.c local initialisation function (AT6558_localInit()), add calls to PS_Transaction_init() for each transaction in the rt_RxTransactions[] array.

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,
            AT6558_CONTAINER_TASKING_TYPE(pt_AT6558));
    }
+
+   /* If no errors, for each receive transaction */
+   for (ui32_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;
}

The AT6558_CONTAINER_TASK_QUEUE_RECEIVE() macro passes the receive Task's queue to the PS_Transaction_init() initialisation function. By passing the task queue to PS_Transaction_init(), the transaction will be placed on that queue when it is completed.

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 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 issuing asynchronous PS receive operations through the AT6558_PS_receiveAsyncBus() function, you will also need to associate the rru8_Buffers buffers with each transaction.

Add calls to the 'PS_Transaction_setReceiveBuffer' function to set the receive buffer then call AT6558_connectionInit() for each transaction:

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 (ui32_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 function calls described in the process 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 */
+   status_t t_Status;
+
+   /* If we have received a PS transaction */
+   if (pv_QueueContext != NULL)
+   {
+       PS_Transaction_t *pt_Transaction;
+       ui8_t *pu8_Buffer;
+       ui32_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)
+               {
+                   /* Parse the GGA sentence out of the received NMEA data */
+                   parseNmea(pt_AT6558, pu8_Buffer, u32_Size);
+               }
+               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);
+           }
+       }
+   }
+   else
+   {
+       /* Something went wrong as this task should only be triggered when we
+        * receive a PS transaction, update status */
+       t_Status = STATUS_FAILURE;
+   }
}

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.

So far, you have implemented Asynchronous PS Service requirement by:

  1. Declaring an array, rt_RxTransactions[], of PS transactions in the component type structure.

  2. Declaring an array, rru8_Buffers[][], of buffers in the component type structure.

  3. Calling the PS transaction initialisation function (PS_Transaction_init()) in the AT6558 Component Type local initialsation function (AT6558_localInit()) to associate the receive Task with each transaction.

  4. Calling the PS_Transaction_setReceiveBuffer() function in the AT6558 PS connection initialisation function (AT6558_connectionInit()) to associate a buffer with each transaction.

  5. Calling the AT6558_PS_receiveAsyncBus() function in the AT6558 PS connection initialisation function (AT6558_connectionInit()) to start receiving transactions.

  6. Calling the PS_Transaction_getReceiveBuffer() and PS_Transaction_getCompletionStatus() functions in the receive Task function (AT6558_taskReceive()) to determine whether valid data was received.

  7. Calling the PS_Transaction_reset() function in the receive Task function (AT6558_taskReceive()) to make the transaction ready for reuse.

  8. Calling the PS_Transaction_setReceiveBuffer() and AT6558_PS_receiveAsyncBus() functions in the receive Task function (AT6558_taskReceive()) to allow transactions to 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.

Implementing a parser can be quite time consuming. In the interest of keeping this tutorial short, we have written two functions that perform the parsing for you. Let’s add them to our implementation file so that we can make use of them within the receive Task.

Add the following two parsing functions to AT6558.c, placing them in a private functions section at the top of the implementation, above the public functions

Show the two parsing functions
/*---------------------------------------------------------------------------*
 * Global variables
 *---------------------------------------------------------------------------*/

/** The lock access timeout */
static const ShortTime_t gt_LockTimeout = AT6558_CONFIG_LOCK_TIMEOUT;

+ /*---------------------------------------------------------------------------*
+  * Private functions
+  *---------------------------------------------------------------------------*/
+
+ /**
+  * Parses the latitude and longitude values out of a GGA sentence.
+  * @param pt_AT6558 The component instance.
+  */
+ static void parseGga
+ (
+     AT6558_t *pt_AT6558
+ )
+ {
+     ui32_t u32_BytesRead;
+     ui8_t u8_FieldBytesRead;
+     AT6558_GgaParsingState_t t_ParsingState;
+     char_t *pc_SentenceBuffer;
+     ui32_t *pu32_SentenceBytes;
+     boolean_t *pb_SentenceValid;
+     float32_t *pf32_Latitude;
+     float32_t *pf32_Longitude;
+
+     /* Extract state variables for readability */
+     pc_SentenceBuffer = pt_AT6558->rc_SentenceBuffer;
+     pu32_SentenceBytes = &pt_AT6558->u32_SentenceBytes;
+     pb_SentenceValid = &pt_AT6558->b_SentenceValid;
+     pf32_Latitude = &pt_AT6558->f32_Latitude;
+     pf32_Longitude = &pt_AT6558->f32_Longitude;
+
+     /* No bytes read yet */
+     u32_BytesRead = 0;
+
+     /* No bytes read in current field yet */
+     u8_FieldBytesRead = 0;
+
+     /* Start reading from the beginning of the sentence */
+     t_ParsingState = AT6558_GGA_PARSING_STATE_ID;
+
+     /* While the sentence is valid and there are still bytes left to read */
+     while ((*pb_SentenceValid) &&
+            (u32_BytesRead < *pu32_SentenceBytes))
+     {
+         /* Which field is currently being read? */
+         switch (t_ParsingState)
+         {
+             case AT6558_GGA_PARSING_STATE_LAT:
+
+                 /* If the end of the field has been reached */
+                 if (pc_SentenceBuffer[u32_BytesRead] == ',')
+                 {
+                     /* If the correct number of bytes have been read */
+                     if (u8_FieldBytesRead == AT6558_NMEA_LAT_SIZE)
+                     {
+                         ui32_t u32_Index;
+                         ui32_t u32_Deg;
+                         float32_t f32_Min;
+
+                         /* Calculate the index for the start of the latitude
+                          * value */
+                         u32_Index = u32_BytesRead - u8_FieldBytesRead;
+
+                         /* Extract the degrees value */
+                         Util_String_u32FromDec(
+                             (const char_t *)&pc_SentenceBuffer[u32_Index],
+                             AT6558_NMEA_LAT_DEG_SIZE,
+                             &u32_Deg);
+
+                         /* Increment index to the minutes value */
+                         u32_Index += AT6558_NMEA_LAT_DEG_SIZE;
+
+                         /* Extract the minutes value */
+                         f32_Min = (float32_t)strtod(
+                             (const char_t *)&pc_SentenceBuffer[u32_Index],
+                             NULL);
+
+                         /* Update state variable  */
+                         *pf32_Latitude =
+                             (float32_t)u32_Deg +
+                             (f32_Min / AT6558_MINUTES_PER_DEGREE);
+                     }
+                     else
+                     {
+                         /* Latitude unknown, set to invalid value */
+                         *pf32_Latitude = AT6558_MAX_LATITUDE + 1;
+                     }
+
+                     /* Increment to next field */
+                     t_ParsingState += 1;
+
+                     /* Reset number of bytes read in current field to zero */
+                     u8_FieldBytesRead = 0;
+                 }
+                 else
+                 {
+                     /* Increment number of bytes read in current field */
+                     u8_FieldBytesRead += 1;
+                 }
+                 break;
+
+             case AT6558_GGA_PARSING_STATE_LAT_DIR:
+
+                 /* If the end of the field has been reached */
+                 if (pc_SentenceBuffer[u32_BytesRead] == ',')
+                 {
+                     /* If the correct number of bytes have been read */
+                     if (u8_FieldBytesRead == AT6558_NMEA_LAT_DIR_SIZE)
+                     {
+                         ui32_t u32_Index;
+
+                         /* Calculate the index for the start of the latitude
+                          * direction */
+                         u32_Index = u32_BytesRead - u8_FieldBytesRead;
+
+                         /* If direction is valid */
+                         if (pc_SentenceBuffer[u32_Index] == 'N' ||
+                             pc_SentenceBuffer[u32_Index] == 'S')
+                         {
+                             /* If direction is south */
+                             if (pc_SentenceBuffer[u32_Index] == 'S')
+                             {
+                                 /* Update state variable */
+                                 *pf32_Latitude *= (-1.0);
+                             }
+                         }
+                         else
+                         {
+                             /* Direction unknown, set latitude to invalid
+                              * value */
+                             *pf32_Latitude = AT6558_MAX_LATITUDE + 1;
+                         }
+                     }
+                     else
+                     {
+                         /* Direction unknown, set latitude to invalid value */
+                         *pf32_Latitude = AT6558_MAX_LATITUDE + 1;
+                     }
+
+                     /* Increment to next field */
+                     t_ParsingState += 1;
+
+                     /* Reset number of bytes read in current field to zero */
+                     u8_FieldBytesRead = 0;
+                 }
+                 else
+                 {
+                     /* Increment number of bytes read in current field */
+                     u8_FieldBytesRead += 1;
+                 }
+                 break;
+
+             case AT6558_GGA_PARSING_STATE_LON:
+
+                 /* If the end of the field has been reached */
+                 if (pc_SentenceBuffer[u32_BytesRead] == ',')
+                 {
+                     /* If the correct number of bytes have been read */
+                     if (u8_FieldBytesRead == AT6558_NMEA_LON_SIZE)
+                     {
+                         ui32_t u32_Index;
+                         ui32_t u32_Deg;
+                         float32_t f32_Min;
+
+                         /* Calculate the index for the start of the longitude
+                          * value */
+                         u32_Index = u32_BytesRead - u8_FieldBytesRead;
+
+                         /* Extract the degrees value */
+                         Util_String_u32FromDec(
+                             (const char_t *)&pc_SentenceBuffer[u32_Index],
+                             AT6558_NMEA_LON_DEG_SIZE,
+                             &u32_Deg);
+
+                         /* Increment index to the minutes value */
+                         u32_Index += AT6558_NMEA_LON_DEG_SIZE;
+
+                         /* Extract the minutes value */
+                         f32_Min = (float32_t)strtod(
+                             (const char_t *)&pc_SentenceBuffer[u32_Index],
+                             NULL);
+
+                         /* Update state variable */
+                         *pf32_Longitude =
+                             (float32_t)u32_Deg +
+                             (f32_Min / AT6558_MINUTES_PER_DEGREE);
+                     }
+                     else
+                     {
+                         /* Longitude unknown, set to invalid value */
+                         *pf32_Longitude = AT6558_MAX_LONGITUDE + 1;
+                     }
+
+                     /* Increment to next field */
+                     t_ParsingState += 1;
+
+                     /* Reset number of bytes read in current field to zero */
+                     u8_FieldBytesRead = 0;
+                 }
+                 else
+                 {
+                     /* Increment number of bytes read in current field */
+                     u8_FieldBytesRead += 1;
+                 }
+                 break;
+
+             case AT6558_GGA_PARSING_STATE_LON_DIR:
+
+                 /* If the end of the field is reached */
+                 if (pc_SentenceBuffer[u32_BytesRead] == ',')
+                 {
+                     /* If the correct number of bytes have been read */
+                     if (u8_FieldBytesRead == AT6558_NMEA_LON_DIR_SIZE)
+                     {
+                         ui32_t u32_Index;
+
+                         /* Calculate the index for the start of the longitude
+                          * direction */
+                         u32_Index = u32_BytesRead - u8_FieldBytesRead;
+
+                         /* If direction is valid */
+                         if (pc_SentenceBuffer[u32_Index] == 'E' ||
+                             pc_SentenceBuffer[u32_Index] == 'W')
+                         {
+                             /* If direction is west */
+                             if (pc_SentenceBuffer[u32_Index] == 'W')
+                             {
+                                 /* Update state variable */
+                                 *pf32_Longitude *= (-1.0);
+                             }
+                         }
+                         else
+                         {
+                             /* Direction unknown, set longitude to invalid
+                              * value */
+                             *pf32_Longitude = AT6558_MAX_LONGITUDE + 1;
+                         }
+                     }
+                     else
+                     {
+
+                         /* Direction unknown, set longitude to invalid value */
+                         *pf32_Longitude = AT6558_MAX_LONGITUDE + 1;
+                     }
+
+                     /* Increment to next field */
+                     t_ParsingState += 1;
+
+                     /* Reset number of bytes read in current field to zero */
+                     u8_FieldBytesRead = 0;
+                 }
+                 else
+                 {
+                     /* Increment number of bytes read in current field */
+                     u8_FieldBytesRead += 1;
+                 }
+                 break;
+
+             default:
+
+                 /* If the end of the field is reached */
+                 if (pc_SentenceBuffer[u32_BytesRead] == ',')
+                 {
+                     /* Increment to next field */
+                     t_ParsingState += 1;
+
+                     /* Reset number of bytes read in current field to zero */
+                     u8_FieldBytesRead = 0;
+                 }
+                 break;
+         }
+
+         /* Increment number of bytes read */
+         u32_BytesRead += 1;
+     }
+ }
+
+ /**
+  * Parses a GGA sentence from a buffer containing multiple NMEA sentences.
+  * @param pt_AT6558 The component instance.
+  * @param pu8_Buffer A buffer containing NMEA data.
+  * @param u32_Size The number of bytes of data within the buffer.
+  */
+ static void parseNmea
+ (
+     AT6558_t *pt_AT6558,
+     const ui8_t *pu8_Buffer,
+     ui32_t u32_Size
+ )
+ {
+     status_t t_Status;
+     ui32_t u32_BytesRead;
+     AT6558_NmeaParsingState_t *pt_ParsingState;
+     char_t *pc_SentenceBuffer;
+     ui32_t *pu32_SentenceBytes;
+     ui8_t *pu8_SentenceChecksum;
+     boolean_t *pb_SentenceValid;
+
+     /* Extract state variables for readability */
+     pt_ParsingState = &pt_AT6558->t_ParsingState;
+     pc_SentenceBuffer = pt_AT6558->rc_SentenceBuffer;
+     pu32_SentenceBytes = &pt_AT6558->u32_SentenceBytes;
+     pu8_SentenceChecksum = &pt_AT6558->u8_SentenceChecksum;
+     pb_SentenceValid = &pt_AT6558->b_SentenceValid;
+
+     /* No errors yet */
+     t_Status = STATUS_SUCCESS;
+
+     /* No bytes read yet */
+     u32_BytesRead = 0;
+
+     /* While there are still bytes left to read */
+     while (u32_BytesRead < u32_Size)
+     {
+         /* Which state are we in? */
+         switch (*pt_ParsingState)
+         {
+             /* State: Waiting for start character */
+             case AT6558_NMEA_PARSING_STATE_START:
+
+                 /* If start character is reached */
+                 if (pu8_Buffer[u32_BytesRead] == '$')
+                 {
+                     /* Reset number of bytes in the sentence buffer */
+                     *pu32_SentenceBytes = 0;
+
+                     /* Reset sentence validity */
+                     *pb_SentenceValid = FALSE;
+
+                     /* Reset computed checksum value */
+                     *pu8_SentenceChecksum = 0;
+
+                     /* Update state to waiting for identifier */
+                     *pt_ParsingState =
+                         AT6558_NMEA_PARSING_STATE_ID;
+                 }
+                 break;
+
+             /* State: Waiting for identifier */
+             case AT6558_NMEA_PARSING_STATE_ID:
+
+                 /* Store the current byte in the sentence buffer */
+                 pc_SentenceBuffer[*pu32_SentenceBytes] =
+                     (char_t)pu8_Buffer[u32_BytesRead];
+
+                 /* If a comma has been reached */
+                 if (pu8_Buffer[u32_BytesRead] == ',')
+                 {
+                     const char_t rc_ValidId[] = {'G', 'G', 'A'};
+
+                     /* If sentence identifier equals GGA */
+                     if (strncmp(
+                             (const char_t *)&pc_SentenceBuffer[2],
+                             rc_ValidId,
+                             ARRAY_COUNT(rc_ValidId)) == 0)
+                     {
+                         /* Update state to waiting for end character */
+                         *pt_ParsingState =
+                             AT6558_NMEA_PARSING_STATE_END;
+                     }
+                     else
+                     {
+                         /* Update state to waiting for start character */
+                         *pt_ParsingState =
+                             AT6558_NMEA_PARSING_STATE_START;
+                     }
+                 }
+
+                 /* Update checksum */
+                 *pu8_SentenceChecksum ^= pc_SentenceBuffer[*pu32_SentenceBytes];
+
+                 /* Increment number of bytes in the sentence buffer */
+                 *pu32_SentenceBytes += 1;
+                 break;
+
+             /* State: Waiting for end character */
+             case AT6558_NMEA_PARSING_STATE_END:
+
+                 /* Store the current byte in the sentence buffer */
+                 pc_SentenceBuffer[*pu32_SentenceBytes] =
+                     (char_t)pu8_Buffer[u32_BytesRead];
+
+                 /* If an asterisk has been reached */
+                 if (pu8_Buffer[u32_BytesRead] == '*')
+                 {
+                     /* Update state to waiting for checksum */
+                     *pt_ParsingState =
+                         AT6558_NMEA_PARSING_STATE_CHECKSUM;
+                 }
+                 else
+                 {
+                     /* Update checksum */
+                     *pu8_SentenceChecksum ^=
+                         pc_SentenceBuffer[*pu32_SentenceBytes];
+                 }
+
+                 /* Increment number of bytes in the sentence buffer */
+                 *pu32_SentenceBytes += 1;
+                 break;
+
+             /* State: Waiting for checksum */
+             case AT6558_NMEA_PARSING_STATE_CHECKSUM:
+
+                 /* If carriage return has been reached */
+                 if (pu8_Buffer[u32_BytesRead] == '\r')
+                 {
+                     char_t rc_AsciiChecksum[AT6558_NMEA_CHECKSUM_SIZE];
+
+                     /* Convert computed checksum value into ASCII
+                      * representation */
+                     t_Status = Util_String_u32ToHex(
+                         *pu8_SentenceChecksum,
+                         rc_AsciiChecksum,
+                         ARRAY_COUNT(rc_AsciiChecksum));
+
+                     /* If no errors */
+                     if (t_Status == STATUS_SUCCESS)
+                     {
+                         ui32_t u32_Index;
+
+                         /* Calculate the index for the start of the checksum */
+                         u32_Index =
+                             *pu32_SentenceBytes - AT6558_NMEA_CHECKSUM_SIZE;
+
+                         /* If computed checksum value is equal to the actual
+                          * checksum value */
+                         if (strncmp(
+                                 (const char_t *)&pc_SentenceBuffer[u32_Index],
+                                 (const char_t *)rc_AsciiChecksum,
+                                 AT6558_NMEA_CHECKSUM_SIZE) == 0)
+                         {
+                             /* Mark sentence as valid */
+                             *pb_SentenceValid = TRUE;
+
+                             /* Parse the latitude and longitude values out of
+                              * the GGA sentence */
+                             parseGga(pt_AT6558);
+                         }
+                     }
+
+                     /* Update state to waiting for start character */
+                     *pt_ParsingState =
+                         AT6558_NMEA_PARSING_STATE_START;
+                 }
+                 else
+                 {
+                     /* Store the current byte in the sentence buffer */
+                     pc_SentenceBuffer[*pu32_SentenceBytes] =
+                         (char_t)pu8_Buffer[u32_BytesRead];
+
+                     /* Increment number of bytes in the sentence buffer */
+                     *pu32_SentenceBytes += 1;
+                 }
+                 break;
+
+             /* State: Default */
+             default:
+
+                 /* Never expect to reach this branch */
+                 break;
+         }
+
+         /* Increment number of bytes read from the received data */
+         u32_BytesRead++;
+     }
+ }
+
/*---------------------------------------------------------------------------*
 * Public functions
 *---------------------------------------------------------------------------*/

Generate the AT6558 Component Type.

With the parser implemented, 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 parser 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. As NMEA sentences may be split between multiple transactions, multiple Task executions might be required to parse one sentence.

  2. Latitude and longitude values will need to be stored external to the Task's 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_types.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 */
    ui8_t rru8_Buffers[AT6558_CONFIG_RX_TRXS][AT6558_CONFIG_TRX_BUFFER_LEN];
+   /** The current state of the parsing process */
+   AT6558_NmeaParsingState_t t_ParsingState;
+   /** A buffer for storing the most recently received sentence */
+   char_t rc_SentenceBuffer[AT6558_NMEA_MAX_SENTENCE_SIZE];
+   /** The number of bytes contained within the sentence buffer */
+   ui32_t u32_SentenceBytes;
+   /** The computed checksum value for the data in the sentence buffer */
+   ui8_t u8_SentenceChecksum;
+   /** A flag indicating if the most recently received sentence is valid */
+   boolean_t b_SentenceValid;
+   /** Most recent latitude value */
+   float32_t f32_Latitude;
+   /** Most recent longitude value */
+   float32_t f32_Longitude;
}
AT6558_t;

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;
+
+   /* Initialise state variables */
+   pt_AT6558->t_ParsingState = AT6558_NMEA_PARSING_STATE_START;
+   pt_AT6558->u32_SentenceBytes = 0;
+   pt_AT6558->u8_SentenceChecksum = 0;
+   pt_AT6558->b_SentenceValid = FALSE;
+   pt_AT6558->f32_Latitude = AT6558_MAX_LATITUDE + 1.0;
+   pt_AT6558->f32_Longitude = AT6558_MAX_LONGITUDE + 1.0;

    /* 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,
            AT6558_CONTAINER_TASKING_TYPE(pt_AT6558));
    }

    /* If no errors, for each receive transactions */
    for (ui32_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;
}

Now let’s define all the new macros that we have introduced, placing them in a new defines section at the top of the AT6558_types.h header.

Add the following list of macro definitions to a new defines section at the top of AT6558_types.h:

Show the changes to AT6558_types.h
+ /*---------------------------------------------------------------------------*
+  * Defines
+  *---------------------------------------------------------------------------*/
+
+ #define AT6558_NMEA_MAX_SENTENCE_SIZE       82
+ #define AT6558_NMEA_HEADER_SIZE             5
+ #define AT6558_NMEA_LAT_DEG_SIZE            2
+ #define AT6558_NMEA_LAT_MIN_SIZE            8
+ #define AT6558_NMEA_LAT_SIZE                AT6558_NMEA_LAT_DEG_SIZE + \
+                                             AT6558_NMEA_LAT_MIN_SIZE
+ #define AT6558_NMEA_LAT_DIR_SIZE            1
+ #define AT6558_NMEA_LON_DEG_SIZE            3
+ #define AT6558_NMEA_LON_MIN_SIZE            8
+ #define AT6558_NMEA_LON_SIZE                AT6558_NMEA_LON_DEG_SIZE + \
+                                             AT6558_NMEA_LON_MIN_SIZE
+ #define AT6558_NMEA_LON_DIR_SIZE            1
+ #define AT6558_NMEA_CHECKSUM_SIZE           2
+ #define AT6558_MAX_LATITUDE                 90.0
+ #define AT6558_MIN_LATITUDE                 -90.0
+ #define AT6558_MAX_LONGITUDE                180.0
+ #define AT6558_MIN_LONGITUDE                -180.0
+ #define AT6558_MINUTES_PER_DEGREE           60.0

/*---------------------------------------------------------------------------*
 * Type definitions
 *---------------------------------------------------------------------------*/

To use some of the functions which are called, we need to include their respective header files. Let’s update the list of include directives in AT6558.c to facilitate this.

Add the following header files to the list of include directives at the top of AT6558.c:

#include "core/types.h"
#include "core/status.h"
#include "AT6558/AT6558.h"
#include "AT6558/AT6558_Container_package.h"
#include "AT6558/AT6558_config.h"
#include "AT6558/AT6558_PS_package.h"
#include "task/Task_ProtectionLock.h"
#include "util/Util_Log.h"
+ #include "util/Util_String.h"
+ #include <string.h>
+ #include <stdlib.h>

Lastly, the functions provided introduce two new data types which aren’t defined, AT6558_NmeaParsingState_t and AT6558_GgaParsingState_t.

Let’s provide definitions for them in AT6558_types.h.

Add the following type definitions to AT6558_types.h:

/** The AT6558 component initialisation data */
typedef struct
{
    /* TODO: Add component initialisation data here */
}
AT6558_Init_t;
+
+ /** Type representing the parsing state for NMEA data */
+ typedef enum
+ {
+     AT6558_NMEA_PARSING_STATE_START = 0,
+     AT6558_NMEA_PARSING_STATE_ID,
+     AT6558_NMEA_PARSING_STATE_END,
+     AT6558_NMEA_PARSING_STATE_CHECKSUM
+ }
+ AT6558_NmeaParsingState_t;
+
+ /** Type representing the parsing state for GGA data */
+ typedef enum
+ {
+     AT6558_GGA_PARSING_STATE_ID = 0,
+     AT6558_GGA_PARSING_STATE_TIME,
+     AT6558_GGA_PARSING_STATE_LAT,
+     AT6558_GGA_PARSING_STATE_LAT_DIR,
+     AT6558_GGA_PARSING_STATE_LON,
+     AT6558_GGA_PARSING_STATE_LON_DIR,
+     AT6558_GGA_PARSING_STATE_QUALITY,
+     AT6558_GGA_PARSING_STATE_SATS,
+     AT6558_GGA_PARSING_STATE_HDOP,
+     AT6558_GGA_PARSING_STATE_ALT,
+     AT6558_GGA_PARSING_STATE_ALT_UNIT,
+     AT6558_GGA_PARSING_STATE_GEOID,
+     AT6558_GGA_PARSING_STATE_GEOID_UNIT,
+     AT6558_GGA_PARSING_STATE_DGPS_TIME,
+     AT6558_GGA_PARSING_STATE_DGPS_STATION,
+     AT6558_GGA_PARSING_STATE_CHECKSUM,
+ }
+ AT6558_GgaParsingState_t;

With all member variables declared and initialised, 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 (as 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->f32_Latitude >= AT6558_MIN_LATITUDE) &&
+           (pt_AT6558->f32_Latitude <= AT6558_MAX_LATITUDE))
+       {
+           /* 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 (as 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->f32_Longitude >= AT6558_MIN_LONGITUDE) &&
+           (pt_AT6558->f32_Longitude <= AT6558_MAX_LONGITUDE))
+       {
+           /* 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;
}

Finally, we need to address the lingering TODO comment in AT6558.types.h which refers to the Initialisation Data structure. This Component Type doesn’t 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 demo_pi Deployment Type

With the AT6558 Component Type complete, it can now be instantiated in a Deployment Type.

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 deployment.xml file found in demo_pi.

If you completed the previous tutorial, some Component Types from 'demo_pi' are likely to be removed already. Add and remove Component Types as needed in the next step to match the deployment provided below.

Remove component instances for the io.bus.spi.SPIMaster, io.bus.i2c.I2CMaster, subsys.ArduCAM and io.driver.GPIO Component Types from the deployment.xml file. Add the following two component instances to the bottom of the <Implementation> element:

Show the component instances added to the deploymentType.xml file.
<?xml version="1.0" encoding="UTF-8"?>
<!--
    This file describes the demo_pi deployment.
-->
<ModelElement xmlns="http://www.brightascension.com/schemas/gen1/model"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <Deployment name="demo_pi">
    <Description>
      This is an example deployment of GenerationOne for the Raspberry PI.
    </Description>
    <!-- Component type imports -->
    <Import>
      <!-- The component types used by this deployment -->
      <Use type="Version" />
      <Use type="storage.Storage" />
      <Use type="ConfigManager" />
      <Use type="io.driver.OSTime" />
      <Use type="storage.config.FileConfigStore" />
      <Use type="storage.fs.PosixFS" />
      <Use type="storage.FileStorageProvider" />
      <Use type="storage.FileSystemManager" />
      <Use type="event.EventDispatcher" />
      <Use type="io.net.tcp.TCPServer" />
      <Use type="io.net.PacketStream" />
      <Use type="io.net.SpacePacket" />
      <Use type="io.net.pus.PUSCore" />
      <Use type="io.net.pus.PUSHK" />
      <Use type="io.net.pus.PUSEvent" />
      <Use type="io.net.pus.PUSLDT" />
      <Use type="io.net.pus.PUSAMS" />
      <Use type="io.net.CFDP" />
      <Use type="tmtc.TMBeacon" />
      <Use type="tmtc.TMDebug" />
      <Use type="tmtc.TMTCEvent" />
      <Use type="io.net.ParamTransfer" />
      <Use type="component.AAS.AASTarget" />
      <Use type="component.PAS.PASTarget" />
      <Use type="subsys.OBT" />
      <Use type="Dummy" />
      <Use type="logging.CommandLogger" />
      <Use type="logging.EventLogger" />
      <Use type="DataPool" />
      <Use type="Sampler" />
      <Use type="Aggregator" />
      <Use type="Monitor" />
      <Use type="logging.DataLogger" />
      <Use type="auto.EventAction" />
      <Use type="auto.TimeAction" />
-     <Use type="io.bus.spi.SPIMaster" />
-     <Use type="io.bus.i2c.I2CMaster" />
-     <Use type="subsys.ArduCAM" />
-     <Use type="io.driver.GPIO" />
+     <Use type="AT6558"/>
+     <Use type="io.bus.Serial"/>
    </Import>
    <!-- Component deployment -->
    <Deploy>
      <!-- Component Groups -->
      <ComponentGroup name="cdh">
        <Description>
          Command and data handling group.
        </Description>
        <Documentation>
          <Text>
            Contains the components used to assist with the function of the
            spacecraft such as loggers, autonomous components and TMTC components.
          </Text>
        </Documentation>
      </ComponentGroup>
      <ComponentGroup name="cdh.logging">
        <Description>
          Group containing the logging components.
        </Description>
        <Documentation>
          <Markdown>
            For demo_linux, the included logging components are:
            - BaseLogger (used for logging data)
            - EventLogger (used to log raised events)
            - TCLogger (used for logging Telecommands)
          </Markdown>
        </Documentation>
      </ComponentGroup>
      <ComponentGroup name="cdh.tmtc">
        <Description>
          Group containing the components that handle TMTC.
        </Description>
        <Documentation>
          <Text>
            TMTC Components are used to connect the comms stack to the components
            so that action/parameter/event interactions can occur.
          </Text>
        </Documentation>
      </ComponentGroup>
      <ComponentGroup name="comms">
        <Description>
          The communications stack group.
        </Description>
        <Documentation>
          <Markdown>
            Contains the components used to form the comms stack.
            For demo_linux, this includes:
            - TCP Client
            - PacketStream
            - SpacePacket
            - PUS components
          </Markdown>
        </Documentation>
      </ComponentGroup>
      <ComponentGroup name="comms.pus">
        <Description>
          Group containing the PUS components.
        </Description>
        <Documentation>
          <Text>
            PUS components are used to provide functionality that corresponds to
            the PUS standard. See ECSS-E-70-41A.
          </Text>
        </Documentation>
      </ComponentGroup>
      <ComponentGroup name="comms.services">
        <Description>
          Group containing the service proxy components.
        </Description>
        <Documentation>
          <Text>
            Service proxies allow services to be provided to external systems and
            allow this deployment to access services provided externally.
          </Text>
        </Documentation>
      </ComponentGroup>
      <ComponentGroup name="core">
        <Description>
          Group containing the core components for the deployment.
        </Description>
        <Documentation>
          <Text>
            Contains components integral to the functionality of the OBSW.
          </Text>
        </Documentation>
      </ComponentGroup>
      <ComponentGroup name="platform">
        <Description>
          Group containing the components integral to the platform being deployed.
        </Description>
        <Documentation>
          <Text>
            For demo_linx, no platform specific hardware or software is required,
            however DummySubsys can be used to simulate TMTC with a component.
          </Text>
        </Documentation>
      </ComponentGroup>
      <!-- Component Instances -->
      <Component name="Version" type="Version" />
      <Component name="core.Storage" type="storage.Storage" />
      <Component name="core.ConfigurationManager" type="ConfigManager" />
      <Component name="core.Time" type="io.driver.OSTime" />
      <Component name="core.OBT" type="subsys.OBT">
        <Connections>
          <Services>
            <Service name="time" component="core.Time" service="time" />
          </Services>
        </Connections>
      </Component>
      <Component name="core.FileSystem" type="storage.fs.PosixFS" />
      <Component name="core.FileConfigStore" type="storage.config.FileConfigStore">
        <Connections>
          <Services>
            <Service name="fs" component="core.FileSystem" service="fs" />
          </Services>
        </Connections>
      </Component>
      <Component name="core.FileStorageProvider" type="storage.FileStorageProvider">
        <Connections>
          <Services>
            <Service name="fs" component="core.FileSystem" service="fs" />
          </Services>
        </Connections>
      </Component>
      <Component name="core.FileSystemManager" type="storage.FileSystemManager">
        <Connections>
          <Services>
            <Service name="fs" component="core.FileSystem" service="fs" />
          </Services>
        </Connections>
        <Tasks>
          <SporadicTask name="decompress" priority="3" />
        </Tasks>
      </Component>
      <Component name="core.EventDispatcher" type="event.EventDispatcher">
        <Tasks>
          <PeriodicTask name="dispatcher" period="1.0" priority="2" />
        </Tasks>
      </Component>
      <Component name="comms.TCPServer" type="io.net.tcp.TCPServer">
        <Tasks>
          <PeriodicTask name="receive" priority="3" />
          <SporadicTask name="receiveTimeout" priority="3" />
        </Tasks>
      </Component>
      <Component name="comms.PacketStream" type="io.net.PacketStream">
        <Connections>
          <Services>
            <Service name="stream" component="comms.TCPServer" service="data" />
          </Services>
        </Connections>
        <Tasks>
          <SporadicTask name="receive" priority="3" />
          <SporadicTask name="receiveTimeout" priority="3" />
        </Tasks>
      </Component>
      <Component name="comms.SpacePacket" type="io.net.SpacePacket">
        <Connections>
          <Services>
            <Service name="spacePacket" component="comms.PacketStream" service="packet" />
          </Services>
        </Connections>
        <Tasks>
          <SporadicTask name="transmit" priority="3" />
          <SporadicTask name="transmitTimeout" priority="3" />
          <SporadicTask name="receive" priority="3" />
          <SporadicTask name="receiveTimeout" priority="3" />
        </Tasks>
      </Component>
      <Component name="comms.pus.PUSCore" type="io.net.pus.PUSCore">
        <Connections>
          <Services>
            <Service name="pusPacket" component="comms.SpacePacket" service="dataPacket" channel="0" />
          </Services>
        </Connections>
        <Tasks>
          <SporadicTask name="transmit" priority="3" />
          <SporadicTask name="transmitTimeout" priority="3" />
          <SporadicTask name="receive" priority="3" />
          <SporadicTask name="receiveTimeout" priority="3" />
        </Tasks>
      </Component>
      <Component name="comms.pus.PUSHK" type="io.net.pus.PUSHK">
        <Connections>
          <Services>
            <Service name="pusSend" component="comms.pus.PUSCore" service="dataPacket" channel="0" />
            <Service name="pusReceive" component="comms.pus.PUSCore" service="dataPacket"
              channel="0" />
          </Services>
        </Connections>
        <Tasks>
          <SporadicTask name="transmit" priority="3" />
          <SporadicTask name="receive" priority="3" />
        </Tasks>
      </Component>
      <Component name="comms.pus.PUSEvent" type="io.net.pus.PUSEvent">
        <Connections>
          <Services>
            <Service name="pusSend" component="comms.pus.PUSCore" service="dataPacket" channel="1" />
            <Service name="pusReceive" component="comms.pus.PUSCore" service="dataPacket"
              channel="1" />
          </Services>
        </Connections>
        <Tasks>
          <SporadicTask name="transmit" priority="3" />
          <SporadicTask name="receive" priority="3" />
        </Tasks>
      </Component>
      <Component name="comms.pus.PUSLDT" type="io.net.pus.PUSLDT">
        <Connections>
          <Services>
            <Service name="pusPacket" component="comms.pus.PUSCore" service="dataPacket" channel="2" />
          </Services>
        </Connections>
        <Tasks>
          <SporadicTask name="transmit" priority="3" />
          <SporadicTask name="transmitTimeout" priority="3" />
          <SporadicTask name="transmitTransferTimeout" priority="3" />
          <SporadicTask name="receive" priority="3" />
          <SporadicTask name="receiveTimeout" priority="3" />
          <SporadicTask name="receiveTransferTimeout" priority="3" />
        </Tasks>
      </Component>
      <Component name="comms.pus.PUSToAAS" type="io.net.pus.PUSAMS">
        <Connections>
          <Services>
            <Service name="pusSend" component="comms.pus.PUSCore" service="dataPacket" channel="4" />
            <Service name="pusReceive" component="comms.pus.PUSCore" service="dataPacket"
              channel="4" />
            <Service name="target" component="comms.services.AASTarget" service="remote" />
          </Services>
        </Connections>
        <Tasks>
          <SporadicTask name="transmit" priority="3" />
          <SporadicTask name="receive" priority="3" />
          <SporadicTask name="requestCompleteTarget" priority="3" />
        </Tasks>
      </Component>
      <Component name="comms.CFDP" type="io.net.CFDP">
        <Connections>
          <Services>
            <Service name="fs" component="core.FileSystem" service="fs" />
            <Service name="ut" component="comms.SpacePacket" service="dataPacket" channel="1" />
          </Services>
        </Connections>
        <Tasks>
          <SporadicTask name="transmit" priority="3" />
          <SporadicTask name="receive" priority="3" />
          <SporadicTask name="timer" priority="3" />
          <SporadicTask name="remoteRequestComplete" priority="3" />
        </Tasks>
      </Component>
      <Component name="comms.services.AASTarget" type="component.AAS.AASTarget" />
      <Component name="comms.pus.PUSToPAS" type="io.net.pus.PUSAMS">
        <Connections>
          <Services>
            <Service name="pusSend" component="comms.pus.PUSCore" service="dataPacket" channel="5" />
            <Service name="pusReceive" component="comms.pus.PUSCore" service="dataPacket"
              channel="5" />
            <Service name="target" component="comms.services.PASTarget" service="remote" />
          </Services>
        </Connections>
        <Tasks>
          <SporadicTask name="transmit" priority="3" />
          <SporadicTask name="receive" priority="3" />
          <SporadicTask name="requestCompleteTarget" priority="3" />
        </Tasks>
      </Component>
      <Component name="comms.services.PASTarget" type="component.PAS.PASTarget" />
      <Component name="comms.pus.PUSToParamTransfer" type="io.net.pus.PUSAMS">
        <Connections>
          <Services>
            <Service name="pusSend" component="comms.pus.PUSCore" service="dataPacket" channel="6" />
            <Service name="pusReceive" component="comms.pus.PUSCore" service="dataPacket"
              channel="6" />
            <Service name="target" component="comms.ParamTransfer" service="remote" />
          </Services>
        </Connections>
        <Tasks>
          <SporadicTask name="transmit" priority="3" />
          <SporadicTask name="receive" priority="3" />
          <SporadicTask name="requestCompleteTarget" priority="3" />
        </Tasks>
      </Component>
      <Component name="comms.ParamTransfer" type="io.net.ParamTransfer">
        <Connections>
          <Services>
            <Service name="ldt" component="comms.pus.PUSLDT" service="dataPacket" />
          </Services>
        </Connections>
        <Tasks>
          <SporadicTask name="transmit" priority="3" />
          <SporadicTask name="receive" priority="3" />
        </Tasks>
      </Component>
      <Component name="cdh.tmtc.TMBeacon" type="tmtc.TMBeacon">
        <Connections>
          <Services>
            <Service name="hkHandler" component="comms.pus.PUSHK" service="hkReporting" />
          </Services>
        </Connections>
        <Tasks>
          <PeriodicTask name="send" period="5.0" priority="2" />
        </Tasks>
      </Component>
      <Component name="cdh.tmtc.TMDebug" type="tmtc.TMDebug">
        <Connections>
          <Services>
            <Service name="pusSend" component="comms.pus.PUSCore" service="dataPacket" channel="3" />
          </Services>
        </Connections>
        <Tasks>
          <SporadicTask name="transmit" priority="3" />
        </Tasks>
      </Component>
      <Component name="cdh.tmtc.TMTCEvent" type="tmtc.TMTCEvent">
        <Connections>
          <Components>
            <Component name="eventHandler" component="comms.pus.PUSEvent" />
          </Components>
        </Connections>
      </Component>
      <Component name="cdh.logging.TCLogger" type="logging.CommandLogger">
        <Connections>
          <Services>
            <Service name="timestamp" component="core.Time" service="time" />
          </Services>
        </Connections>
        <Tasks>
          <PeriodicTask name="store" period="10.0" priority="2" />
        </Tasks>
      </Component>
      <Component name="platform.DummySubsys1" type="Dummy" />
      <Component name="platform.PlatformSPI" type="io.bus.spi.SPIMaster">
        <Description>
          The main SPI bus for interfacing to the test SPI
        </Description>
      </Component>
-      <Component name="platform.PlatformI2C" type="io.bus.i2c.I2CMaster">
-        <Description>
-          Platform I2C bus
-        </Description>
-      </Component>
-      <Component name="platform.ArduCAM" type="subsys.ArduCAM">
-        <Connections>
-          <Services>
-            <Service name="arduChip" component="platform.PlatformSPI" service="data" channel="0" />
-            <Service name="sensor" component="platform.PlatformI2C" service="data" channel="0" />
-            <Service name="fs" component="core.FileSystem" service="fs" />
-          </Services>
-        </Connections>
-        <Tasks>
-          <SporadicTask name="capture" priority="3" />
-        </Tasks>
-      </Component>
-      <Component name="platform.GPIO" type="io.driver.GPIO" />
+      <Component name="UART" type="io.bus.Serial">
+        <Description>
+          Platform's PL011 UART which interfaces with the serial bus.
+        </Description>
+        <Tasks>
+          <InterruptTask name="receive" priority="3"/>
+          <InterruptTask name="transmit" priority="3"/>
+          <SporadicTask name="transmitTimeout" priority="3"/>
+          <SporadicTask name="receiveTrxTimeoutTask" priority="3"/>
+        </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="3"/>
+        </Tasks>
+      </Component>
      <Component name="cdh.logging.EventLogger" type="logging.EventLogger">
        <Connections>
          <Services>
            <Service name="timestamp" component="core.Time" service="time" />
          </Services>
        </Connections>
        <Tasks>
          <PeriodicTask name="store" period="10.0" priority="2" />
        </Tasks>
      </Component>
      <Component name="cdh.DataPool" type="DataPool">
        <Connections>
          <Services>
            <Service name="time" component="core.Time" service="time" />
          </Services>
        </Connections>
        <ParameterAliases>
          <ParameterBlock blockName="poolParameters">
            <ComponentParameter name="DummySubsys1" component="platform.DummySubsys1"
              parameter="dummyParam8" />
            <ComponentParameter name="DummySubsys1" component="platform.DummySubsys1"
              parameter="dummyParam16" />
            <ComponentParameter name="DummySubsys1" component="platform.DummySubsys1"
              parameter="dummyParam32" />
          </ParameterBlock>
        </ParameterAliases>
      </Component>
      <Component name="cdh.BeaconAggregator" type="Aggregator" />
      <Component name="cdh.BaseSampler" type="Sampler">
        <Connections>
          <Components>
            <Component name="DataPool" component="cdh.DataPool" />
          </Components>
        </Connections>
        <Tasks>
          <PeriodicTask name="sample" period="5.0" priority="2" />
        </Tasks>
      </Component>
      <Component name="cdh.BaseAggregator" type="Aggregator" />
      <Component name="cdh.BaseMonitor" type="Monitor">
        <Tasks>
          <PeriodicTask name="refresh" period="5.0" priority="2" />
        </Tasks>
      </Component>
      <Component name="cdh.logging.BaseLogger" type="logging.DataLogger">
        <Connections>
          <Services>
            <Service name="timestamp" component="core.Time" service="time" />
          </Services>
        </Connections>
        <Tasks>
          <PeriodicTask name="sample" period="10.0" priority="2" />
          <PeriodicTask name="store" period="60.0" priority="2" />
        </Tasks>
      </Component>
      <Component name="cdh.EventAction" type="auto.EventAction" />
      <Component name="cdh.TimeAction" type="auto.TimeAction">
        <Connections>
          <Services>
            <Service name="time" component="core.Time" service="time" />
          </Services>
        </Connections>
        <Tasks>
          <PeriodicTask name="main" period="1.0" priority="2" />
        </Tasks>
      </Component>
    </Deploy>
  </Deployment>
</ModelElement>

Remember to update the Deployment's project.mk to include the components library you created earlier.

# Dependencies (library directories)
DEPEND_DIRS = ../app ../framework components

You can generate documentation to find out what requirements Component Types have for initialisation.

You can generate the documentation for the framework module by opening a terminal in the fsdk-22.2/OBSW/Source directory and executing the following command:

codegen module generate-docs framework

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 demo_pi Deployment using following command:

codegen deployment generate demo_pi

This produces an SCDB which will allow you to interact with the Deployment via tmtclab. 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 Deployment, you must set the Initialisation Data for the new component instances in their respective Deployments.

Open demo_pi/src/init/UART_Init.c.

You must specify the Initialisation Data for the UART component instance as per the Serial_Init_t structure defined in Serial.h.

The first member to set is z_Device. As 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. To communicate with the unit, you must configure the UART to use the same rate.

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 demo_pi/src/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 Flight Software Development Kit flight software on your Pi you will need to follow the steps in our

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 demo_pi using the following command in the fsdk-22.2/OBSW/Source directory:

make -C demo_pi/ force target

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

demo_pi/bin/pi3/demo_pi

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 demo_pi/bin/pi3/demo_pi pi@raspberrypi:

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

Using another terminal, use SSH to connect to the Pi using the following command. Again, 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:

./demo_pi

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

INF: Main.c:67 Platform initialisation successful
INF: Deployment.c:36 Deployment initialisation successful

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

Open tmtclab, load the SCDB and connect to the Deployment.

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 Parameter get requests are returning error code 2, indicating an invalid parameter, it is likely that the GPS device does not have a valid locational fix.

If this occurs, try positioning the GPS device in a location with a clearer view of the sky and wait 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 Flight Software Development Kit tutorials on Component Type development. Hopefully you now have a good grasp of the fundamentals of building Component Types with Flight Software Development Kit.

If you haven’t already reviewed the guides and reference material provided with FSDK, you can do so now:

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

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