Menu

Expand
Rate this page:

A Guide to Microvisor Networking

Microvisor Public Beta

Microvisor is in a pre-release phase and the information contained in this document is subject to change. Some features referenced below may not be fully available until Microvisor’s General Availability (GA) release.

Microvisor owns all of the device’s networking sub-systems which can be used to connect the device to the Internet. Microvisor makes these network sub-systems available to the application through the Microvisor System Calls to allow your code to transfer data to and from remote servers.

This guide will help you understand how this is achieved by walking you through a typical usage flow:

  • Establish a network connection.
  • Set up a data channel.
  • Send and receive data through the channel.
  • Close the channel.
  • End the network connection.

Using networking

At a high level, your application asks Microvisor to provide it with a network connection. When it has done so, Microvisor passes the application a handle which uniquely identifies the connection. The application can use this handle to check the connection’s status and to open two-way data pathways, called channels, that are hosted by the connection. The application might use a channel to request data from a cloud service and to read the response. When it has completed the interaction, the application closes the channel. If it no longer needs the network connection, it relinquishes its access.

Like the network connection, every channel is identified by its own handle. Each handle’s value is a 32-bit unsigned integer that is unique for the lifetime of the resource to which it has been assigned.

Handles are used to specify all types of resource in application-Microvisor interactions. When you open a channel, you provide the handle of the network connection that will host it: you include the network connection handle in the channel configuration data. Microvisor will provide a channel handle in return: use this channel handle for all future work with the channel.

Before using a handle, you should always check that it is not zero. Any handle with the value zero is invalid. All extant handles are zeroed when Microvisor boots, and specific handles are zeroed when the resource they refer to is relinquished or closed by the application through the appropriate system call. If you attempt to make use of an invalid handle, Microvisor will report it as an error.

Network ownership

When Microvisor starts up, it will attempt to establish a network connection. It will maintain this connection until your application takes ownership of the network state by requesting network connectivity. From this point, it is your application’s responsibility to manage when the device is connected to the network and when it is not.

Microvisor will take advantage of the application-managed connection to check if application or system updates have been staged for download. If the application intentionally goes offline, or if it doesn’t reconnect after an outage, Microvisor will autonomously connect when it needs to make one of its periodic check-ins with Twilio. It will stay connected for as short a time as possible.

Microvisor will also connect if the application crashes. This puts ownership of network state back in the hands of Microvisor until your code once again makes a network request.

A special case is remote debugging. In this case, Microvisor maintains a connection for debugging even if the application disconnects.

The System Call mvGetNetworkReasons() provides a means for your application to determine the state of network ownership. It can be called at any time, even if your application hasn’t yet requested access to the network. The call writes out a bitfield of flags that together indicate current networking status. For example, examining the bitfield after the application has closed the network connection might reveal that Microvisor nonetheless has a connection in place because a remote debugger is attached.

You can view all the flags that may be set in the bitfield in the mvGetNetworkReasons() documentation.

Application logging

Microvisor’s application logging system implicitly makes use of any available connection to relay posted messages to the Twilio Cloud, from when they will be streamed to any remote computer that’s listening for them. This means that it’s not necessary for your application to request network connectivity solely to have posted log messages relayed. However, if you do request network access for other reasons, and you close the connection, messages will not be relayed.

Log messages posted while the device is offline — intentionally or unintentionally — are buffered so long as their is sufficient space in the buffer for them. Buffered messages will be sent to the cloud as soon as connectivity is restored by the current owner.

Requesting network connectivity

To request a network connection, you application calls mvRequestNetwork(). This has these parameters:

  • A pointer to a data structure which allows you to specify how Microvisor will notify your application about subsequent network operations.
  • A pointer to application-accessible memory into which Microvisor will write the network connection’s handle.

Microvisor will now bring up the network connection, powering up the hardware and making a connection to the Internet if these are not already in place. Once this process is complete, Microvisor will signal network availability to the application by dispatching an MV_EVENTTYPE_NETWORKSTATUSCHANGED notification.

For more information on when and how Microvisor issues notifications to the application, and how the application can register its interest in certain notifications, please see Microvisor Notifications.

With a network connection in place, the application is ready to open channels and use them to transfer data.

Like all Microvisor system calls, mvRequestNetwork() immediately returns a status value indicating whether Microvisor is able to proceed with the request. Only continue if Microvisor returns the value MV_STATUS_OKAY.

Checking network status

You can check on the status of a network connection at any time by calling mvGetNetworkStatus() and passing both the handle of the network you’re interested in and the address of memory into which Microvisor will write a connection status code which will be one of the following:

Status Description
MV_NETWORKSTATUS_DELIBERATELYOFFLINE The device is not connected
MV_NETWORKSTATUS_CONNECTED The device is connected
MV_NETWORKSTATUS_CONNECTING The device is connecting to the Internet

Example code

First, you need to establish the notification center to which network notifications will be posted. This center will then be used to configure the network connection itself. The code also shows how to set up stores for the various types of handle that will be used, and for notifications.

// Central store for Microvisor resource handles used in this code.
// See 'https://www.twilio.com/docs/iot/microvisor/syscalls#handles'
struct {
  MvNotificationHandle notification;
  MvNetworkHandle      network;
  MvChannelHandle      channel;
} net_handles = { 0, 0, 0 };

// Central store for notification records. Holds one record at
// a time -- each record is 16 bytes in size.
static volatile struct MvNotification net_notification_buffer[16];

// Clear the notification store
memset((void *)net_notification_buffer, 0xff, sizeof(net_notification_buffer));

// Configure a notification center for network-centric notifications
static struct MvNotificationSetup net_notification_config = {
  .irq = TIM1_BRK_IRQn,
  .buffer = (struct MvNotification *)net_notification_buffer,
  .buffer_size = sizeof(net_notification_buffer)
};

// Ask Microvisor to establish the notification center
// and confirm that it has accepted the request
enum MvStatus status = mvSetupNotifications(&net_notification_config, 
                                            &net_handles.notification);
assert(status == MV_STATUS_OKAY);

// Start the notification IRQ
NVIC_ClearPendingIRQ(TIM1_BRK_IRQn);
NVIC_EnableIRQ(TIM1_BRK_IRQn);

Here is the code you would write to open a network connection:

// Configure the network connection request
struct MvRequestNetworkParams network_config = {
  .version = 1,
  .v1 = {
    .notification_handle = net_handles.notification,
    .notification_tag = USER_TAG_LOGGING_REQUEST_NETWORK,
  }
};

// Ask Microvisor to establish the network connection
// and confirm that it has accepted the request
enum MvStatus status = mvRequestNetwork(&network_config, 
                                        &net_handles.network);
assert(status == MV_STATUS_OKAY);

However, because the connection is established asynchronously, it’s important to check network status before proceeding to open the channel — which will fail if the connection is not yet ready:

// The network connection is established by Microvisor asynchronously,
// so we wait for it to come up before opening the data channel -- which
// would fail otherwise
enum MvNetworkStatus net_status;
while (1) {
  // Request the status of the network connection, identified by its handle.
  // If we're good to continue, break out of the loop...
  if (mvGetNetworkStatus(net_handles.network, &net_status) == MV_STATUS_OKAY && 
        net_status == MV_NETWORKSTATUS_CONNECTED) {
    break;
  }

  // ... or wait a short period before retrying
  for (volatile unsigned i = 0; i < 50000; ++i) {
    // No op
    __asm("nop");
  }
}

Recall that Microvisor system calls return immediately with a value — status in the code above — that indicates whether the request was accepted or rejected, not whether the request itself succeeded or failed.

General connection state checks

A call to mvGetNetworkStatus() can be used at any time to determine the state of the device’s connection. This code assumes your network connection’s handle is stored in a variable called network_handle.

// Check connection state
bool is_connected = false;
if (net_handles.network != 0) {
  enum MvNetworkStatus net_state = MV_NETWORKSTATUS_DELIBERATELYOFFLINE;
  uint32_t status = mvGetNetworkStatus(net_handles.network, &net_state);
  if (status == MV_STATUS_OKAY) {
    is_connected = (net_state == MV_NETWORKSTATUS_CONNECTED);
  }
}

Working with channels

To open a channel, call mvOpenChannel(). This function takes a data structure that’s used to configure the channel, and a pointer to memory where Microvisor will write the channel’s handle.

The configuration data comprises:

  • Pointers to the channel’s send and receive buffers created by the application.
  • The sizes of those buffers.
  • The channel type.
  • The handle of the network connection that will host the channel.

The network connection that is hosting the channel must be connected to the Internet or the attempt to open the channel will fail. If connectivity is subsequently lost, any open channels on the network will be closed automatically, and your application will receive an EVENT_NETWORK_STATUS_CHANGED notification. This should trigger a call to mvGetNetworkStatus().

Channel types

The channel type value tells Microvisor how the channel is to be used: the protocol it should use to send and receive information. At this time, only opaque bytes and HTTPS – specifed by the constants MV_CHANNELTYPE_OPAQUEBYTES and MV_CHANNELTYPE_HTTP, respectively — are supported, but we expect to support the MQTT protocol in due course.

This guide focuses on the opaque bytes channel type. To learn how to use HTTP channel types to make common HTTP requests and manage their responses, please see How to Issue HTTP Requests under Microvisor.

Endpoints

The channel configuration structure includes an endpoint property and the related endpoint_len. This is expected to be used to specify the name of a cloud-stored configuration. This configuration will indicate a channel’s target server and the credentials required to access that resource. This will be done to ensure that the application code need not hold high-value credentials. However, this functionality is not yet implemented.

Send and receive buffers

The application is responsible for allocating its own channel send and receive buffers. However, once Microvisor has opened a channel, it isn’t possible to change the sizes or addresses of the channel’s buffers. If you need to do so, you must close the channel and open a new one.

Each buffer is treated by Microvisor as circular. It will maintain a pointer and move it forward as data is written and read. When the pointer reaches the end of the buffer, it continues from at the start of the buffer once more.

As data leaves the send buffer, space is made for further data to be sent. Microvisor keeps track of the available space for you. As you read data from the receive buffer it’s important to inform Microvisor that you have done so, so that it knows how much capacity the buffer has for incoming data. We’ll see how that’s done in a moment.

Buffers must be sized in multiples of 512 bytes and their addresses aligned to 512-byte boundaries.

Example code

Having already requested a network connection and ensured that it is up before proceeding, we can now open the channel:

Set up the channel’s send and receive buffers with appropriate sizes and alignment:

// Set up the HTTP channel's multi-use send and receive buffers
static volatile uint8_t rx_buffer[RX_BUFFER_SIZE_B] __attribute__((aligned(512)));
static volatile uint8_t tx_buffer[TX_BUFFER_SIZE_B] __attribute__((aligned(512)));
static const char endpoint[] = "";

Now add those buffers to the channel definition:

struct MvOpenChannelParams channel_config = {
  .version = 1,
  .v1 = {
    .notification_handle = net_handles.notification,
    .notification_tag    = USER_TAG_OPEN_CHANNEL,
    .network_handle      = net_handles.network,
    .receive_buffer      = (uint8_t*)rx_buffer,
    .receive_buffer_len  = sizeof(rx_buffer),
    .send_buffer         = (uint8_t*)tx_buffer,
    .send_buffer_len     = sizeof(tx_buffer),
    .channel_type        = MV_CHANNELTYPE_OPAQUEBYTES,
    .endpoint            = (uint8_t*)endpoint,
    .endpoint_len        = 0
  }
};

Finally, request the channel be opened:

// Ask Microvisor to open the channel
// and confirm that it has accepted the request
enum MvStatus status = mvOpenChannel(&channel_config, &net_handles.channel);
if (status == MV_STATUS_OKAY) {
  server_log("Channel handle: %lu", (uint32_t)net_handles.channel);
} else {
  server_error("Channel opening failed. Status: %i", status);
}

Sending data

You write the data you want to send not directly to the send buffers but via Microvisor. This is because Microvisor manages the movement of data out of the buffer and onto the network. It write-protects the send buffer while the channel is open.

To send data, the application calls mvWritechannel(), a function with the following parameters:

  • The handle of an open channel — writing to a closed channel will result in an error, MV_STATUS_CHANNELCLOSED.
  • A pointer to the bytes to be sent.
  • The number of bytes to be sent.
  • A pointer to application-accessible memory into which Microvisor write the number of bytes available in the send buffer after the write operation has completed.

Microvisor copies the bytes into the channel’s send buffer from where they will be asynchronously sent out on the network. When the data has been copied, Microvisor informs the application exactly how much free space the send buffer has remaining by writing this value to the second pointer passed into mvWritechannel().

If all of the data provided by the application can’t fit in the send buffer, Microvisor doesn’t add any data to the buffer, but does write the number of bytes available in the send buffer. It also issues an error. Your application can respond to this error by sending sufficient bytes to fill the available buffer space, but it’s better to check the number available bytes after every send and use mvWritechannel() to write no more than that amount.

Whenever the amount of space in the buffer increases because data has been sent out to the network, Microvisor posts an MV_EVENTTYPE_CHANNELDATAWRITESPACE notification. This lets the application know that it can send more data, or to wait for all data to be fully sent before closing the channel.

Flushing a channel

To flush a channel, the application should wait for the MV_EVENTTYPE_CHANNELDATAWRITESPACE notification and check in the notification handler whether the available send space reported by mvWritechannel() matches the size of the send buffer. If it does, the buffer is clear.

A call to mvWritechannel() with zero specified as the number of bytes to be sent is treated by Microvisor as a request for the amount of free space in the send buffer. As an alternative to the notification-based approach, you can poll mvWritechannel() and again check the reported available space.

Example code

The following code sends a timestamp into the channel that you created. It then writes out a carriage return. Both parts use mvWritechannel() to send the strings:

// Set the timestamp string to send
char message[64] = {0};
uint64_t usec = 0;
time_t sec = 0;
time_t msec = 0;
enum MvStatus status = mvGetWallTime(&usec);
if (status == MV_STATUS_OKAY) {
  // Get the second and millisecond times
  sec = (time_t)usec / 1000000;
  msec = (time_t)usec / 1000;
}

// Write time string as "2022-05-10 13:30:58.XXX "
strftime(message, 64, "%F %T.XXX ", gmtime(&sec));

// Insert the millisecond time over the XXX
sprintf(&message[20], "%03u", (unsigned)(msec % 1000));

// Write out the message string and then send a convenient
// carriage return too. Each time confirm that Microvisor has
// accepted the request to write data to the channel.
uint32_t available, status;
status = mvWriteChannel(net_handles.channel, 
                        (const uint8_t*)message,
                        strlen(message),
                        &available);
assert(status == MV_STATUS_OKAY);

// Write a <CR> at the end
status = mvWriteChannel(net_handles.channel, 
                        (const uint8_t*)"\n", 
                        1,
                        &available);
assert(status == MV_STATUS_OKAY);

Streaming data

The mvWritechannel() system call allows you to write discrete chunks of information. What if you want to transmit a flow of data? For this you use mvWritechannelStream(). This call lets you send a queue data for transmission. You pass:

  • The handle of an open channel — writing to a closed channel will result in an error, MV_STATUS_CHANNELCLOSED.
  • A pointer to the bytes to be sent.
  • The number of bytes to be sent.
  • A pointer to application-accessible memory into which Microvisor will write the number of bytes it was able to send.

Microvisor will attempt to send the data, but if there is insufficient free space in its TX buffer, it does not issue an error. Instead, it reports the number of bytes it did send, if any. You can then top up your application’s FIFO with that number of bytes and call mvWritechannelStream() again. At each call, Microvisor will add as many bytes as it can into the TX buffer and report that number. The MV_EVENTTYPE_CHANNELDATAWRITESPACE notification will inform you when the amount of free space in the buffer increases.

Receiving data

Typically, the application accesses received data in response to receiving a MV_EVENTTYPE_CHANNELDATAREADABLE notification from Microvisor, but the application can read data at any time. Whenever it does so, it calls mvReadchannel() which has the following parameters:

  • The handle of an open channel.
  • A pointer to application-accessible memory into which Microvisor will write a pointer to the new data.
  • A pointer to application-accessible memory into which Microvisor will write the number of bytes available to be read.

The application can now read any or all of the available bytes. When it has finished — however many bytes it has read — it informs Microvisor of this by calling mvReadchannelComplete() and passing in the channel handle along with the number of bytes it consumed. This lets Microvisor calculate how much space is now available in the channel’s RX buffer for incoming data.

Closing channels

When you have finished with a channel, call mvClosechannel() to release it. It will zero the channel handle to prevent the use of that handle again — doing so will cause Microvisor to issue an error.

Example code

Closing a channel is straightforward, but it’s prudent to check that the closure request was successful. Your application might need to follow a different path if the response from mvCloseChannel() is not MV_STATUS_OKAY.

// If we have a valid channel handle -- ie. it is non-zero --
// then ask Microvisor to close it and confirm acceptance of
// the closure request.
if (net_handles.channel != 0) {
  enum MvStatusstatus = mvCloseChannel(&net_handles.channel);
    assert(status == MV_STATUS_OKAY);
}

// Confirm the channel handle has been invalidated by Microvisor
assert(net_handles.channel == 0);

Unexpected channel closures

Devices can lose connectivity for a variety of reasons: they are in an area with marginal cellular coverage, or the local network to which they are connected has lost its Internet backhaul. Whatever the reason for the loss of connectivity, it will cause Microvisor to post an MV_EVENTTYPE_CHANNELNOTCONNECTED notification to every open channel. However, Microvisor does not close the channels. This is so that your application can read any data still in their receive buffers, though no new data will be added to them.

You can call mvReadchannel() and mvReadchannelComplete() to get data that was received before the loss of connectivity, and then mvClosechannel() to terminate the channel.

However, you cannot call mvWritechannel(). No data can be sent because there’s no connection, so Microvisor will issue an error if you try to do so.

Close the network connection

Any time that you have a network connection, you can relinquish your application’s access to it by calling mvReleaseNetwork(). It has a single parameter: the network connection handle.

Releasing the network connection will cause it to be closed — but only if there are no other handles that reference it, and Microvisor is not making use of it to check for OS and application updates. If you release a network connection that is host to one or more open channels, those channels will each receive an MV_EVENTTYPE_CHANNELNOTCONNECTED notification as described above.

Example code

Closing the network is straightforward, but don’t forget to remove the notification center assigned to the network if you no longer need it.

// If we have a valid network handle, then ask Microvisor to
// close the connection and confirm acceptance of the request.
if (net_handles.network != 0) {
  status = mvReleaseNetwork(&net_handles.network);
  assert(status == MV_STATUS_OKAY);
}

// Confirm the network handle has been invalidated by Microvisor
assert(net_handles.network == 0);

// If we have a valid notification center handle, then ask Microvisor
// to tear down the center and confirm acceptance of the request.
if (net_handles.notification != 0) {
  status = mvCloseNotifications(&net_handles.notification);
  assert(status == MV_STATUS_OKAY);
}

// Confirm the notification center handle has been invalidated
assert(net_handles.notification == 0);
Rate this page:

Need some help?

We all do sometimes; code is hard. Get help now from our support team, or lean on the wisdom of the crowd by visiting Twilio's Stack Overflow Collective or browsing the Twilio tag on Stack Overflow.

Thank you for your feedback!

Please select the reason(s) for your feedback. The additional information you provide helps us improve our documentation:

Sending your feedback...
🎉 Thank you for your feedback!
Something went wrong. Please try again.

Thanks for your feedback!

Refer us and get $10 in 3 simple steps!

Step 1

Get link

Get a free personal referral link here

Step 2

Give $10

Your user signs up and upgrade using link

Step 3

Get $10

1,250 free SMSes
OR 1,000 free voice mins
OR 12,000 chats
OR more