Reference for C Device SDK
You can use the C interface of the Device SDK to integrate your Devices with Spotflow IoT Platform.
Requirements
You don't need to install additional software to run the Device SDK library on any supported operating system. The example projects use the following tools to compile the sample code that uses the Device SDK library:
- Linux
- Windows
Feel free to use any other toolchain that you're familiar with.
Installation
Download the latest version of the library for the operating system and processor architecture of your choice:
If you need the library compiled for a different operating system or processor architecture, please contact us.
Extract the archive to a directory of your choice.
You can now include the header file include/spotflow.h
into your code.
Then, link the static library file to your project or add the dynamic library file to your project's runtime path.
See the details for each operating system below:
- Linux
- Windows
You'll see the following directory structure:
Directory | Content |
---|---|
bin | Dynamic library file libspotflow.so |
examples | Example program get_started.c that can be compiled using the provided Makefiles - one for using the dynamic library and one for using the static library |
include | Header file spotflow.h |
lib | Static library file libspotflow.a |
To try the basic example, change <Your Provisioning Token>
in examples/get_started.c
to the actual Provisioning Token from the Platform.
Then, navigate to one of the following directories:
examples/gcc_makefile_dynamic
to use the dynamic library.examples/gcc_makefile_static
to use the static library.
Run the command:
make run
This will compile the example program and run it. You should see the request to approve the Device into the Platform. See Get Started for more details on approving the Device and viewing the received data.
If you want to remove the compiled files, run:
make clean
You'll see the following directory structure:
Directory | Content |
---|---|
bin | Dynamic library file spotflow.dll |
examples | Example program get_started.c that can be compiled using the provided Makefiles - one for using the dynamic library and one for using the static library |
include | Header file spotflow.h |
lib | Static library file spotflow.lib and the import library spotflow.dll.lib |
To try the basic example, open one of the provided solutions in Visual Studio 2022:
examples/vs2022_dynamic/spotflow_example.sln
to use the dynamic library.examples/vs2022_static/spotflow_example.sln
to use the static library.
In Solution Explorer, expand Source Files and double-click get_started.c
to open it.
Change <Your Provisioning Token>
in examples/get_started.c
to the actual Provisioning Token from the Platform.
Press F5 or Debug > Start Debugging to build and run the example program.
This will compile the example program and run it. You should see the request to approve the Device into the Platform. See Get Started for more details on approving the Device and viewing the received data.
If you want to remove the compiled files, right-click the project spotflow_example in Solution Explorer and select Clean.
Basic Usage
The following code is the contents of the file examples/get_started.c
that is included in the download archive (see Installation).
It connects the Device to the Platform and starts sending simulated sensor measurements.
You need to register to the Platform and set it up before you can use the code.
See Get Started for more information on configuring the Platform, registering your Device, and viewing the received data.
Don't forget to replace <Your Provisioning Token>
with the actual Provisioning Token from the Platform.
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include "spotflow.h"
#ifdef _WIN32
#include <windows.h>
#define gmtime_r(timer, buf) gmtime_s(buf, timer)
#else
#include <unistd.h>
#define Sleep(x) usleep((x)*1000)
#endif
void show_last_error()
{
size_t error_size = SPOTFLOW_ERROR_MAX_LENGTH;
char* error_buffer = malloc(error_size);
spotflow_read_last_error_message(error_buffer, error_size);
printf("Error: %s\n", error_buffer);
free(error_buffer);
}
void send_data(spotflow_client_t* client)
{
spotflow_message_context_t* ctx;
spotflow_message_context_create(&ctx, "default-stream-group", "default-stream");
const size_t max_size = 1024;
char* msg = malloc(max_size);
for (int i = 0; i < 60; i++)
{
time_t now_ts = time(NULL);
struct tm now;
gmtime_r(&now_ts, &now);
double temperature = 21 + (i * 0.05);
double humidity = 50 + (i * 0.1);
snprintf(
msg, max_size,
"{\"timestamp\": \"%04d-%02d-%02dT%02d:%02d:%02dZ\", \"temperatureCelsius\": %g, \"humidityPercent\": %g}",
now.tm_year + 1900, now.tm_mon + 1, now.tm_mday, now.tm_hour, now.tm_min, now.tm_sec, temperature, humidity);
printf("%s\n", msg);
if (spotflow_client_send_message(client, ctx, NULL, NULL, (const uint8_t*)msg, strlen(msg)) != SPOTFLOW_OK)
{
show_last_error();
return;
}
Sleep(5000);
}
free(msg);
spotflow_message_context_destroy(ctx);
}
int main()
{
spotflow_client_options_t* options;
spotflow_client_options_create(&options, "my-device", "<Your Provisioning Token>", "spotflow.db");
spotflow_client_t* client;
if (spotflow_client_start(&client, options) != SPOTFLOW_OK)
{
show_last_error();
return 1;
}
send_data(client);
spotflow_client_destroy(client);
spotflow_client_options_destroy(options);
}
Reference
Functions
Set the verbosity level of logging (SPOTFLOW_LOG_WARN
by default).
Parameters
level | The verbosity level of logging. |
---|
SPOTFLOW_OK
if successful, SPOTFLOW_ERROR
otherwise.
Write the most recent error message into the provided buffer as a UTF-8 string, returning the number of bytes written. If successful, the error message is consumed and will not be returned again in a future call to this function.
Since the string is in the UTF-8 encoding, Windows users may need to convert it to a UTF-16 string before displaying it.
Parameters
buffer | The buffer to write the error message into. |
---|---|
buffer_length | The length of the buffer in bytes. Use SPOTFLOW_ERROR_MAX_LENGTH to be sure that it is always large enough. |
The number of bytes written into the buffer including the trailing null character. If no error has been produced yet, returns 0. If buffer or buffer_length are invalid (for example, null pointer or insufficient length), returns -1.
spotflow_result_t
spotflow_client_options_create( struct spotflow_client_options_t
** options, const char * device_id, const char * provisioning_token, const char * database_file)Create an object that stores the connection options. The created object of type spotflow_client_options_t
is managed by the Device SDK. After you configure all the options (using either this function or with the additional functions listed below), pass the address of spotflow_client_options_t
to spotflow_client_start
. Delete spotflow_client_options_t
using spotflow_client_options_destroy
.
See also
spotflow_client_options_set_database_file spotflow_client_options_set_provisioning_token spotflow_client_options_set_device_id spotflow_client_options_set_instance spotflow_client_options_set_display_provisioning_operation_callbackParameters
options | (Output) The pointer to the spotflow_client_options_t object that will be created by this function. |
---|---|
device_id | (Optional) The ID of the Device you are running the code from. Use NULL if you don't want to specify it. See spotflow_client_options_set_device_id . |
provisioning_token | The Provisioning Token that will spotflow_client_start use to start Device Provisioning. See spotflow_client_options_set_provisioning_token . |
database_file | The path to the local database file where the Device SDK stores the connection credentials and temporarily persists incoming and outgoing messages. See spotflow_client_options_set_database_file . |
SPOTFLOW_OK
if the function succeeds, SPOTFLOW_ERROR
if any argument is invalid (or NULL if it's required).
spotflow_result_t
spotflow_client_options_set_database_file( struct spotflow_client_options_t
* options, const char * database_file)Set the path to the local database file where the Device SDK stores the connection credentials and temporarily persists incoming and outgoing messages. spotflow_client_start
creates the file if it doesn't exist.
The file must end with the suffix ".db", for example, "spotflow.db". If you don't use an absolute path, the file is created relative to the current working directory.
Parameters
options | The spotflow_client_options_t object. |
---|---|
database_file | The path to the local database file. |
SPOTFLOW_OK
if the function succeeds, SPOTFLOW_ERROR
if any argument is invalid.
spotflow_result_t
spotflow_client_options_set_provisioning_token( struct spotflow_client_options_t
* options, const char * provisioning_token)Set the Provisioning Token that will spotflow_client_start
use to start Device Provisioning.
See Get Started for instructions how to create a Provisioning Token.
Parameters
options | The spotflow_client_options_t object. |
---|---|
provisioning_token | The Provisioning Token. |
SPOTFLOW_OK
if the function succeeds, SPOTFLOW_ERROR
if any argument is invalid.
spotflow_result_t
spotflow_client_options_set_device_id( struct spotflow_client_options_t
* options, const char * device_id)Set the ID of the Device you are running the code from. If you don't specify it here, you'll need to choose it during the approval of the Provisioning Operation. See Device Provisioning for more details about the process.
Make sure that no two Devices in the same Workspace use the same ID. Otherwise, unexpected errors can occur during the communication with the Platform.
Parameters
options | The spotflow_client_options_t object. |
---|---|
device_id | (Optional) The ID of the Device. Use NULL if you don't want to specify it. |
SPOTFLOW_OK
if the function succeeds, SPOTFLOW_ERROR
if any argument is invalid.
spotflow_result_t
spotflow_client_options_set_instance( struct spotflow_client_options_t
* options, const char * instance)Set the URI/hostname of the Platform instance where the Device will connect to.
If your company uses a dedicated instance of the Platform, such as acme.spotflow.io, specify it here. The default value is api.eu1.spotflow.io.
Parameters
options | The spotflow_client_options_t object. |
---|---|
instance | (Optional) The domain of the Platform instance. Use NULL if you want to keep the default value. |
SPOTFLOW_OK
if the function succeeds, SPOTFLOW_ERROR
if any argument is invalid.
spotflow_result_t
spotflow_client_options_set_display_provisioning_operation_callback( struct spotflow_client_options_t
* options, spotflow_display_provisioning_operation_callback_t
callback, void * context)Set the function that displays the details of the Provisioning Operation when spotflow_client_start
is performing Device Provisioning. See Get Started for hands-on experience with this process.
Parameters
options | The spotflow_client_options_t object. |
---|---|
callback | (Optional) The function that displays the details of the Provisioning Operation. Use NULL if you don't want to specify it. |
context | (Optional) The context that will be passed to callback. Use NULL if you don't want to specify it. |
SPOTFLOW_OK
if the function succeeds, SPOTFLOW_ERROR
if any argument is invalid.
Destroy the spotflow_client_options_t
object.
Parameters
options | The spotflow_client_options_t object to destroy. |
---|
spotflow_result_t
spotflow_client_start( spotflow_client_t
** client, const struct spotflow_client_options_t
* options)Start communicating with the Platform. The created object of type spotflow_client_t
is managed by the Device SDK. Delete it using spotflow_client_destroy
.
If the Device is not yet registered in the Platform, or its Registration Token is expired, this method performs Device Provisioning and waits for the approval. See Get Started for the instructions how to set up the Platform, start Device Provisioning, and approve the Device.
If the Registration Token from the last run is still valid, this method succeeds even without the connection to the Internet. The Device SDK will store all outgoing communication in the local database file and send it once it connects to the Platform.
Parameters
client | (Output) The pointer to the spotflow_client_t object that will be created by this function. |
---|---|
options | The options that specify how to connect to the Platform. |
SPOTFLOW_OK
if the function succeeds, SPOTFLOW_ERROR
if any argument is invalid.
spotflow_result_t
spotflow_message_context_create( struct spotflow_message_context_t
** message_context, const char * stream_group, const char * stream)Create an object that stores the options for sending Messages to the Platform.
Initialize the spotflow_message_context_t
object: set the most important fields to the provided values and the other fields to default values. The created object of type spotflow_message_context_t
is managed by the Device SDK. You can configure the object either using this function or using the additional functions listed below. Delete it using spotflow_message_context_destroy
.
See also
spotflow_message_context_set_stream_group spotflow_message_context_set_stream spotflow_message_context_set_compressionParameters
message_context | (Output) The pointer to the spotflow_message_context_t object that will be created by this function. |
---|---|
stream_group | (Optional) The Stream Group the Message will be sent to. If NULL, the Platforms directs the Messages to the default Stream Group of the current Workspace. |
stream | (Optional) The Stream the Message will be sent to. If NULL, the Platform directs the Messages into the default Stream of the given Stream Group. |
SPOTFLOW_OK
if the function succeeds, SPOTFLOW_ERROR
if any argument is invalid.
spotflow_result_t
spotflow_message_context_set_stream_group( struct spotflow_message_context_t
* message_context, const char * stream_group)Set the Stream Group where Messages will be sent to.
Parameters
message_context | The spotflow_message_context_t object. |
---|---|
stream_group | The Stream Group. If NULL, the Platforms directs the Messages to the default Stream Group of the current Workspace. |
SPOTFLOW_OK
if the function succeeds, SPOTFLOW_ERROR
if any argument is invalid.
spotflow_result_t
spotflow_message_context_set_stream( struct spotflow_message_context_t
* message_context, const char * stream)Set the Stream where Messages will be sent to.
Parameters
message_context | The spotflow_message_context_t object. |
---|---|
stream | The Stream. If NULL, the Platform directs the Messages into the default Stream of the given Stream Group. |
SPOTFLOW_OK
if the function succeeds, SPOTFLOW_ERROR
if any argument is invalid.
spotflow_result_t
spotflow_message_context_set_compression( struct spotflow_message_context_t
* message_context, enum spotflow_compression_t
compression)Set the compression to use for sending Messages.
Parameters
message_context | The spotflow_message_context_t object. |
---|---|
compression | The compression to use for sending Messages. |
SPOTFLOW_OK
if the function succeeds, SPOTFLOW_ERROR
if any argument is invalid.
Destroy the spotflow_message_context_t
object.
Parameters
message_context | The spotflow_message_context_t object to destroy. |
---|
spotflow_result_t
spotflow_client_enqueue_message( spotflow_client_t
* client, const struct spotflow_message_context_t
* message_context, const char * batch_id, const char * message_id, const uint8_t * buffer, size_t length)Enqueue a Message to be sent to the Platform.
If the Stream doesn't have a Message ID Autofill Pattern, you must provide message_id. If the Stream groups Messages into Batches and doesn't have a Batch ID Autofill Pattern, you must provide batch_id. See User Guide for more details.
The method returns right after it saves the Message to the queue in the local database file. A background thread asynchronously sends the messages from the queue to the Platform. You can check the number of pending messages in the queue using spotflow_client_get_pending_messages_count
.
Parameters
client | The spotflow_client_t object. |
---|---|
message_context | The options that specify how to send the Message. |
batch_id | (Optional) The ID of the Batch the Message is a part of. Use NULL if you don't want to specify it. |
message_id | (Optional) The ID of the Message. Use NULL if you don't want to specify it. |
buffer | The buffer that contains the Message. |
length | The length of the buffer in bytes. |
SPOTFLOW_OK
if the function succeeds, SPOTFLOW_ERROR
if any argument is invalid or there is an error in persisting the message.
spotflow_result_t
spotflow_client_enqueue_message_advanced( spotflow_client_t
* client, const struct spotflow_message_context_t
* message_context, const char * batch_id, const char * batch_slice_id, const char * message_id, const char * chunk_id, const uint8_t * buffer, size_t length)Enqueue a Message to be sent to the Platform.
If the Stream doesn't have a Message ID Autofill Pattern, you must provide message_id. If the Stream groups Messages into Batches and doesn't have a Batch ID Autofill Pattern, you must provide batch_id. See User Guide for more details. Optionally, you can provide also batch_slice_id to use Batch Slices and chunk_id to use Message Chunking.
The method returns right after it saves the Message to the queue in the local database file. A background thread asynchronously sends the messages from the queue to the Platform. You can check the number of pending messages in the queue using spotflow_client_get_pending_messages_count
.
Parameters
client | The spotflow_client_t object. |
---|---|
message_context | The options that specify how to send the Message. |
batch_id | (Optional) The ID of the Batch the Message is a part of. Use NULL if you don't want to specify it. |
batch_slice_id | (Optional) The ID of the Batch Slice the Message is a part of. Use NULL if you dont' want to specify it. |
message_id | (Optional) The ID of the Message. Use NULL if you don't want to specify it. |
chunk_id | (Optional) The ID of the Chunk of the Message. Use NULL if you don't want to specify it. |
buffer | The buffer that contains the Message. |
length | The length of the buffer in bytes. |
SPOTFLOW_OK
if the function succeeds, SPOTFLOW_ERROR
if any argument is invalid or there is an error in persisting the message.
spotflow_result_t
spotflow_client_enqueue_batch_completion( spotflow_client_t
* client, const struct spotflow_message_context_t
* message_context, const char * batch_id)Enqueue the manual completion of the current Batch to be sent to the Platform.
The Platform also completes the previous Batch automatically when the new one starts. Therefore, you might not need to call this method at all. See User Guide for more details.
The method returns right after it saves the batch-completing Message to the queue in the local database file. A background thread asynchronously sends the messages from the queue to the Platform. You can check the number of pending messages in the queue using spotflow_client_get_pending_messages_count
.
Parameters
client | The spotflow_client_t object. |
---|---|
message_context | The options that specify how to send the batch-completing Message. |
batch_id | The ID of the Batch that will be completed. |
SPOTFLOW_OK
if the function succeeds, SPOTFLOW_ERROR
if any argument is invalid or there is an error in persisting the message.
spotflow_result_t
spotflow_client_enqueue_message_completion( spotflow_client_t
* client, const struct spotflow_message_context_t
* message_context, const char * batch_id, const char * message_id)Enqueue the manual completion of the current Message to be sent to the Platform. Use this methods when Message Chunking is used.
The method returns right after it saves the message-completing Message to the queue in the local database file. A background thread asynchronously sends the Messages from the queue to the Platform. You can check the number of pending messages in the queue using spotflow_client_get_pending_messages_count
.
Parameters
client | The spotflow_client_t object. |
---|---|
message_context | The options that specify how to send the message-completing Message. |
batch_id | The ID of the Batch that the Message belongs to. |
message_id | The ID of the Message. |
SPOTFLOW_OK
if the function succeeds, SPOTFLOW_ERROR
if any argument is invalid or there is an error in persisting the message.
Block the current thread until all the Messages that have been previously enqueued are sent to the Platform.
Parameters
client | The spotflow_client_t object. |
---|
SPOTFLOW_OK
if the function succeeds, SPOTFLOW_ERROR
if the argument is invalid.
spotflow_result_t
spotflow_client_send_message( spotflow_client_t
* client, const struct spotflow_message_context_t
* message_context, const char * batch_id, const char * message_id, const uint8_t * buffer, size_t length)Send a Message to the Platform.
Warning: This method blocks the current thread until the Message (and all the Messages enqueued before it) is sent to the Platform. If your Device doesn't have a stable Internet connection, consider using spotflow_client_enqueue_message instead.
If the Stream doesn't have a Message ID Autofill Pattern, you must provide message_id. If the Stream groups Messages into Batches and doesn't have a Batch ID Autofill Pattern, you must provide batch_id. See User Guide for more details.
Parameters
client | The spotflow_client_t object. |
---|---|
message_context | The options that specify how to send the Message. |
batch_id | (Optional) The ID of the Batch the Message is a part of. Use NULL if you don't want to specify it. |
message_id | (Optional) The ID of the Message. Use NULL if you don't want to specify it. |
buffer | The buffer that contains the Message. |
length | The length of the buffer in bytes. |
SPOTFLOW_OK
if the function succeeds, SPOTFLOW_ERROR
if any argument is invalid or there is an error in persisting the message.
spotflow_result_t
spotflow_client_send_message_advanced( spotflow_client_t
* client, const struct spotflow_message_context_t
* message_context, const char * batch_id, const char * batch_slice_id, const char * message_id, const char * chunk_id, const uint8_t * buffer, size_t length)Send a Message to the Platform.
Warning: This method blocks the current thread until the Message (and all the Messages enqueued before it) is sent to the Platform. If your Device doesn't have a stable Internet connection, consider using spotflow_client_enqueue_message_advanced instead.
If the Stream doesn't have a Message ID Autofill Pattern, you must provide message_id. If the Stream groups Messages into Batches and doesn't have a Batch ID Autofill Pattern, you must provide batch_id. See User Guide for more details. Optionally, you can provide also batch_slice_id to use Batch Slices and chunk_id to use Message Chunking.
Parameters
client | The spotflow_client_t object. |
---|---|
message_context | The options that specify how to send the Message. |
batch_id | (Optional) The ID of the Batch the Message is a part of. Use NULL if you don't want to specify it. |
batch_slice_id | (Optional) The ID of the Batch Slice the Message is a part of. Use NULL if you dont' want to specify it. |
message_id | (Optional) The ID of the Message. Use NULL if you don't want to specify it. |
chunk_id | (Optional) The ID of the Chunk of the Message. Use NULL if you don't want to specify it. |
buffer | The buffer that contains the Message. |
length | The length of the buffer in bytes. |
SPOTFLOW_OK
if the function succeeds, SPOTFLOW_ERROR
if any argument is invalid or there is an error in persisting the message.
spotflow_result_t
spotflow_client_get_pending_messages_count( const spotflow_client_t
* client, size_t * count)Get the number of Messages that have been persisted in the local database file but haven't been sent to the Platform yet.
Parameters
client | The spotflow_client_t object. |
---|---|
count | (Output) The number of pending Messages. |
SPOTFLOW_OK
if the function succeeds, SPOTFLOW_ERROR
if any argument is invalid or there is an error in accessing the local database file.
spotflow_result_t
spotflow_client_get_workspace_id( const spotflow_client_t
* client, char * buffer, size_t buffer_length)Write the ID of the Workspace to which the Device belongs into the provided buffer.
Parameters
client | The spotflow_client_t object. |
---|---|
buffer | The buffer where the Workspace ID string including the trailing NUL character will be written to. |
buffer_length | The length of the buffer in bytes. Use SPOTFLOW_WORKSPACE_ID_MAX_LENGTH to be sure that it is always large enough. |
SPOTFLOW_OK
if the function succeeds, SPOTFLOW_INSUFFICIENT_BUFFER
if the buffer is too small, SPOTFLOW_ERROR
if any argument is invalid.
spotflow_result_t
spotflow_client_get_device_id( const spotflow_client_t
* client, char * buffer, size_t buffer_length)Write the Device ID into the provided buffer. Note that the value might differ from the one requested in spotflow_client_options_set_device_id
if the technician overrides it during the approval of the Provisioning Operation.
Parameters
client | The spotflow_client_t object. |
---|---|
buffer | The buffer where the Device ID string including the trailing NUL character will be written to. |
buffer_length | The length of the buffer in bytes. Use SPOTFLOW_DEVICE_ID_MAX_LENGTH to be sure that it is always large enough. |
SPOTFLOW_OK
if the function succeeds, SPOTFLOW_INSUFFICIENT_BUFFER
if the buffer is too small, SPOTFLOW_ERROR
if any argument is invalid.
Disconnect from the Platform and destroy the spotflow_client_t
object.
Parameters
client | The spotflow_client_t object. |
---|
Enums
A result of a function that can fail. If a function returns a spotflow_result_t
, the caller must check that the returned value is SPOTFLOW_OK
before continuing further. If the function returns a different value, the caller can call spotflow_read_last_error_message
to retrieve the error message. Check the documentation of the particular function to see what results it can return.
The compression to use for sending a message.
The verbosity levels of logging.
Structs
A client connected to the Platform. This object is managed by the Device SDK. Create its instance using spotflow_client_start
and delete it using spotflow_client_destroy
.
The client stores all outgoing communication to the local database file and then sends it in a background thread asynchronously. Thanks to that, it works even when the connection is unreliable. Similarly, the client also stores all ingoing communication to the local database file and deletes it only after the application processes it.
A set of options that specify how to connect to the Platform. This object is managed by the Device SDK. Create its instance using spotflow_client_options_create
and delete it using spotflow_client_options_destroy
. After you configure all the options, pass the address of spotflow_client_options_t
to spotflow_client_start
.
A set of options for sending Messages to a Stream. This object is managed by the Device SDK. Create its instance using spotflow_message_context_create
and delete it using spotflow_message_context_destroy
.
The summary of an ongoing Provisioning Operation. This object is managed by the Device SDK and its contents must not be modified.
If you specify a custom callback to spotflow_client_options_set_display_provisioning_operation_callback
, you'll receive a pointer to spotflow_provisioning_operation_t
as its argument. The pointer is valid only for the duration of the callback.
(Don't modify) The expiration time of this Provisioning Operation. The operation is no longer valid after that.
The date/time format is RFC 3339.
Callbacks
The callback to display the details of an ongoing Provisioning Operation when the Device SDK is performing Device Provisioning. The callback is called only if you have configured it by spotflow_client_options_set_display_provisioning_operation_callback
. The callback is called on the same thread that calls spotflow_client_start
.
Parameters
operation | The summary of the Provisioning Operation. See spotflow_provisioning_operation_t for details. |
---|---|
context | The optional context that was configured by spotflow_client_options_set_display_provisioning_operation_callback . |
Constants
The maximum number of bytes of any Device ID string including the trailing NUL character.
The maximum number of bytes of any Workspace ID string including the trailing NUL character.
The maximum number of bytes of any error message including the trailing null character.