IPC Framework

The IPC framework is based on message-passing. The interface is asynchronous (i.e. non-blocking).

Throughout the documentation, the programs that are written by the user will be referred to as “(user) applications”. They communicate with the services provided by the platform, referred to as “services”. The term “peer” is used to refer to a more generic entity, which may be either a user application or a service.

Quick Example

The following is a minimal example for requesting the Cellular service data resource.

The example will trigger the following message sequence.

*   +------+                          +---------+
*   | user |                          | service |
*   +---+--+                          +----+----+
*       |     ipc_open_session_req         |
*       |--------------------------------->|
*       |     ipc_open_session_rsp         |
*       |<---------------------------------|
*       |     cellular_data_request_req    |
*       |--------------------------------->|
*       |     cellular_data_request_rsp    |
*       |<---------------------------------|
*       .                                  .
*       .                                  .

The code looks as follows. It is available in the demo actia-minimal-ipc-demo.

#define IPC_ID (A_IPC_GUEST_MIN_ID + 6)
#define IPC_OUTBOX_SIZE 10
#define IPC_INBOX_SIZE 15
A_IPC_STATIC_BUFFERS(ipc_buffers, IPC_INBOX_SIZE, IPC_OUTBOX_SIZE);

a_log_set_logger(a_log_stdout_logger);

int ret;
int epfd = epoll_create1(0);
struct epoll_event events[10];
int event_count;
int ipc_fd = 0;

a_ipc_handle *a_ipc = a_ipc_init(IPC_ID, &ipc_buffers);

A_IPC_MSG_ON_STACK(msg_buf, 256);

ipc_fd = a_ipc_get_fd(a_ipc);

events[0].events = EPOLLIN;
events[0].data.fd = ipc_fd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, ipc_fd, events);
if (ret < 0) {
    perror("epoll_ctl ADD ipc_fd");
    return -1;
}

A_IPC_RESULT rc = a_ipc_init_ipc_open_session_req(msg_buf);
if (rc != A_IPC_RET_OK) {
    printf("Failed to create ipc open session request: %s\n", a_ipc_result_str(rc));
    return -2;
}
rc = a_ipc_send(a_ipc, A_SERVICE_ID_CELLULAR, msg_buf);
if (rc != A_IPC_RET_OK) {
    printf("Failed to send open session request: %s\n", a_ipc_result_str(rc));
    return -2;
}

int from;
bool run = true;
do {
    event_count = epoll_wait(epfd, events, a_membersof(events), -1);
    for (int i = 0; i < event_count; i++) {
        if (events[i].data.fd == ipc_fd) {
            rc = a_ipc_recv(a_ipc, &from, msg_buf);
            if (rc != 0) {
                printf("a_ipc_recv error: %d\n", rc);
                continue;
            }
            printf("Received msg type: %s\n",
                    a_ipc_msg_type_str(msg_buf->info.type));
            switch (msg_buf->info.type) {
             case A_IPC_MSG_IPC_OPEN_SESSION_RSP:
                a_ipc_init_cellular_data_request_req(msg_buf, 20);
                msg_buf->cellular_data_request_req.user.value = "demouser";
                msg_buf->cellular_data_request_req.user.length = strlen("demouser");

                rc = a_ipc_send(a_ipc, A_SERVICE_ID_CELLULAR, msg_buf);
                if (rc != A_IPC_RET_OK) {
                    printf("Failed to send open session request: %s\n", a_ipc_result_str(rc));
                    run = false;
                }
                break;
             case A_IPC_MSG_IPC_SESSION_CLOSED_IND:
                run = false;
                break;
             case A_IPC_MSG_CELLULAR_DATA_REQUEST_RSP:
                run = false;
                break;
            default:
                printf("Unknown command: %d\n", msg_buf->info.type);
                break;
            }
        }
    }
} while (run);

a_ipc_destroy(a_ipc);
return rc;

Note that production code should always check for errors and release resources when they are not needed anymore.

Refer to the demo applications for more examples and API Reference.

Functional description

The use of the IPC framework always starts by initializing an instance of the framework, and it ends by destroying the instance. All communication with other peers is performed in sessions.

When sending messages, they must be initialized and filled with data before being sent. To be able to receive messages, a pre-created buffer must be available.

The typical message handling flow looks this:

Initialize IPC
  |
  |<-------------------------------------------------------------------\
  |                                                                    |
  |-> Set up buffer --> Init message --> Set parameters --> Send -----/|
  |                                                                    |
  |-> Set up buffer --> Receive --> Check errors -> Read parameters ---/
  |
  v
Destroy IPC

The typical outline of the application code looks like below, in pseudo-code.

Initialize IPC

init_open_session_req(msg);
send(msg);

while (true) {
   epoll_wait();
   recv(msg);

   switch (msg) {
      case open_session_rsp:
         if (result == OK) {
            // Start communicating
         }
      case session_closed_ind:
         // Peer closed the session
         // Reset state
         // Try to re-open session
      case send_error:
         // Handle error
      case other_msg:
         // Read message parameters
   }
}

Destroy IPC

IPC Initialization and Destruction

The IPC framework is initialized by calling a_ipc_init() which returns a handle to the IPC instance. When done using IPC, the handle shall be destroyed using a_ipc_destroy().

The application needs to allocate inbox and outbox buffers and supply them to the a_ipc_init() call. This can be done with the macro A_IPC_STATIC_BUFFERS, which will allocate the buffers statically. (Static allocation is preferred when targeting smaller devices, such as the ACU6-Lite.)

Each peer must have a system-unique ID, which is used for addressing. IDs available to user applications are given by A_IPC_GUEST_MIN_ID and A_IPC_GUEST_MAX_ID (inclusive). Each service provided by Actia has an ID (e.g. A_SERVICE_ID_CELLULAR).

The IPC framework interface is single-threaded, which means that calls to the framework must be done from the same calling thread. Note that it is possible to create multiple instances of IPC and use them from separate threads.

Messages

Data sent between peers is encapsulated in messages. Each message is of a specific message type (msg->info.type), which in turn belongs to a message class.

A message is represented by an enum constant, for the type ID, and a struct. Refer to Data Types for a description of the types that the message members can have.

An example message with its enum constant and struct:

A_IPC_MSG_RESOURCE_TRIGGER_RSP = 10

typedef struct {
    bool success;
    int counts[10];
    string message;
} a_ipc_msg_resource_trigger_rsp;

The sample below shows how to create a message and fill it with data

a_ipc_msg *global_msg;

void my_function()
{
   A_IPC_STATIC_MSG(static_msg, 256);
   global_msg = static_msg;


   A_IPC_MSG_ON_STACK(msg, 256);

   a_ipc_init_cellular_data_request_req(msg, 20);
   /* msg->info.type is now A_IPC_MSG_CELLULAR_DATA_REQUEST_REQ */

   msg->cellular_data_request_req.user.value = "demouser";
   msg->cellular_data_request_req.user.length = strlen("demouser");

   /* a_ipc_send(a_ipc, PEER_ID, msg); is often called here to
      send the message. */
}

The macros A_IPC_MSG_ON_STACK and A_IPC_STATIC_MSG create buffers of the given size and initializes the message metadata in the info member. Both make the given message an a_ipc_msg pointer. To make a message globally accessible, create it statically and then point a global pointer to it.

The initialization is done using a_ipc_init_<msgtype>() and sets up the message buffer for the requested message type. If the message has dynamic length members (such as strings), the length to reserve for them in the buffer must be given. Refer to Arrays.

A message buffer can be reused multiple times. One can change the values or change the message type by calling a_ipc_init_<msgtype>().

Sessions

To keep track of peer state and restarts, the IPC framework has a session concept. Before doing any other communication with a peer, the application has to open a communication session.

To open a session, the application sends a_ipc_msg_ipc_open_session_req. After the session is successfully set up (given by the result in a_ipc_msg_ipc_open_session_rsp) the application can start communicating with the peer.

To close a session, the application sends a_ipc_msg_ipc_close_session_req_norsp. The session is immediately closed.

If the session is closed by the other peer (explicitly or due to communication problems), the application will receive a_ipc_msg_ipc_session_closed_ind.

An application shall assume that state in the peer is lost when a session is closed (This might not be instant).

It is recommended to close all sessions before calling a_ipc_destroy().

Sending messages

After initializing a message, it can simply be sent by a_ipc_send().

a_ipc_send(a_ipc, PEER_ID, msg);

Receiving messages

Messages are received into an inbox. a_ipc_recv() is used to fetch messages from the inbox. The msg parameter must point to a valid message buffer.

Since a_ipc_recv() is non-blocking, it might return A_IPC_RET_NO_MSG to indicate that no message has been received. Therefore, it’s recommended to use a_ipc_get_fd() to get the IPC file descriptor and listen for when it is ready for reading.

After receiving a message, the type (msg->info.type) can be used to determine how to interpret it and access the correct members. Always check that the return value is A_IPC_RET_OK or A_IPC_RET_SEND_ERROR before reading the message buffer.

A_IPC_STATIC_MSG(msg, 256);

int ipc_fd = a_ipc_get_fd();

/* epoll setup with ipc_fd */

while (true) {
   epoll_wait();

   int from;
   A_IPC_RESULT rc = a_ipc_recv(a_ipc, &from, msg);

   if (rc == A_IPC_RET_OK || rc == A_IPC_RET_SEND_ERROR) {
      switch (msg->info.type) {
         case A_IPC_MSG_CELLULAR_DATA_REQUEST_RSP:
            printf("Request resp: %d\n", msg->cellular_data_request_rsp.result);
            break;
      }
   }
}

Refer to the SDK demo applications for a full example of waiting for and receiving messages.

Send error and Delivery guarantees

All messages are guaranteed to either be delivered or to produce a send error (except if the IPC inbox is full, in which case an error is logged (Log control API).

There are no guarantees on message ordering. Messages in transit can become out of order. The IPC inbox puts each message in the first empty slot and does not track in which order messages were received.

If a message can not be delivered, the application receives an a_ipc_msg_ipc_send_error_ind message. The send error indicates the type of the undeliverable message, in msg_type, and a reason. The cause for send errors is typically that the receiver is not running or a session problem, but could also be due to the receiver’s inbox being full.

When watching for incoming messages, the application should make sure to watch for A_IPC_MSG_IPC_SEND_ERROR_IND.

Message classes

The message class is identified by the suffix of the message type name and indicates its behavior. E.g. foo_ind is an Indication, which means that no response message will be sent by the receiver.

Class

Suffix

Description

Direction

Indication

ind

Message with no response.

service -> application

Request no response

req_norsp

Message with no response.

application -> service

Request

req

Message that expects a corresponding response.

application -> service

Response

rsp

Message in response to a request.

service -> application

Subscriptions

The IPC framework provides a subscription mechanism. It’s a generic method of subscribing to needed messages, and receiving all published messages for a given subscription.

Typically the user application subscribes to IPC messages in its initialization phase.

The application should check the subscription response to make sure that a subscription is established. When data is not needed anymore, the application should unsubscribe.

Subscriptions are automatically removed if the client is not reachable by the service, but it might not happen immediately.

+-------+                         +---------+
|  app  |                         | service |
+---+---+                         +----+----+
    |        foo_subscribe_req         |
    |--------------------------------->|
    |        foo_subscribe_rsp         |
    |<---------------------------------|
    .                                  .
    .                                  .
    |        foo_publish_ind           |
    |<---------------------------------|
    |        foo_publish_ind           |
    |<---------------------------------|
    |        foo_publish_ind           |
    |<---------------------------------|
    .                                  .
    .                                  .
    |    foo_unsubscribe_req_norsp     |
    |--------------------------------->|

Class

Suffix

Description

Publish Indication

publish_ind

New data published from the service.

Subscribe Request

subscribe_req

Subscribe to receive data on new events/periodically.

Subscribe Response

subscribe_rsp

Response to the subscribe request. Indicates if the request was successful.

Unsubscribe Request

unsubscribe_req_norsp

Unsubscribe from data updates.

Data Types

An IPC message can have members of several different types.

Primitive data types

Integers and booleans are stored as their corresponding C type.

Enumerations

Enumeration values are handled in the same way as primitive data types.

Arrays

Arrays build upon the primitive data types. An array can be of either fixed or dynamic length.

Fixed arrays are defined as regular C arrays.

Dynamic length arrays are defined as structs with a C array value field and a length field. length specifies the number of elements in the array.

A buffer for the array will be reserved in the message when initializing it. Therefore the maximum length of dynamic arrays must be given when initializing a message. length is set to the given maximum length on initialization.

It is possible to re-point value to a variable outside the message as long as the variable does not go out of scope before the message is used. The memory of value will be copied when calling a_ipc_send(). Note that length must be updated accordingly.

String

String can be seen as a specialization of an array, with some changes:

  • Strings in received or copied messages are always null-terminated. (Null-terminating input arguments is not needed. Termination is added automatically.)

  • length indicates the length of the string excluding the null-termination.

  • Strings are UTF-8-encoded.

Strings are built of char s and are represented in the same way as arrays.