cancel
Showing results for 
Search instead for 
Did you mean: 
cancel
Showing results for 
Search instead for 
Did you mean: 

Community Tip - Learn all about PTC Community Badges. Engage with PTC and see how many you can earn! X

C SDK Tutorial Part 3

No ratings

 

Step 6: Data Shapes

 

Data Shapes are an important part of creating/firing Events and also invoking Services

 

Define With Macros

 

In order to define a Data Shape using a macro, use TW_MAKE_DATASHAPE.

 

NOTE: The macros are all defined in the twMacros.h header file.

 

TW_MAKE_DATASHAPE("SteamSensorReadingShape",
    TW_DS_ENTRY("ActivationTime", TW_NO_DESCRIPTION ,TW_DATETIME),
    TW_DS_ENTRY("SensorName", TW_NO_DESCRIPTION ,TW_NUMBER),
    TW_DS_ENTRY("Temperature", TW_NO_DESCRIPTION ,TW_NUMBER),
    TW_DS_ENTRY("Pressure", TW_NO_DESCRIPTION ,TW_NUMBER),
    TW_DS_ENTRY("FaultStatus", TW_NO_DESCRIPTION ,TW_BOOLEAN),
    TW_DS_ENTRY("InletValve", TW_NO_DESCRIPTION ,TW_BOOLEAN),
    TW_DS_ENTRY("TemperatureLimit", TW_NO_DESCRIPTION ,TW_NUMBER),
    TW_DS_ENTRY("TotalFlow", TW_NO_DESCRIPTION ,TW_INTEGER)
);

 

Define Without Macros

 

In order to define a Data Shape without using a macro, use the  twDataShape_CreateFromEntries  function. In the example below, we are creating a Data Shape called SteamSensorReadings that has two numbers as Field Definitions.

 

twDataShape * ds = twDataShape_Create(twDataShapeEntry_Create("a",NULL,TW_NUMBER));
twDataShape_AddEntry(ds, twDataShapeEntry_Create("b",NULL,TW_NUMBER));

/* Name the DataShape for the SteamSensorReadings service output */
twDataShape_SetName(ds, "SteamSensorReadings");
 
 

Step 7: Events and Services

 

Events and Services provide useful functionality. Events are a good way to make a Service be asynchronous. You can call a Service, let it return, then your Entity can subscribe to your Event and not keep the original Service function waiting. Events are also a good way to allow the platform to respond to data when it arrives on the edge device without it having to poll the edge device for updates.

 

Fire Events

 

To fire an Event you first need to register the Event and load it with the necessary fields for the Data Shape of that Event using the twApi_RegisterEvent function. Afterwards, you would send a request to the ThingWorx server with the collected values using the twApi_FireEvent function. An example is as follows:

 

twDataShape * ds = twDataShape_Create(twDataShapeEntry_Create("message", NULL,TW_STRING));
    
/* Event datashapes require a name */
twDataShape_SetName(ds, "SteamSensorFault");
/* Register the service */
twApi_RegisterEvent(TW_THING, thingName, "SteamSensorFault", "Steam sensor event", ds);

….

struct  {
    char FaultStatus;
    double Temperature;
    double TemperatureLimit;
} properties;

….

properties. TemperatureLimit = rand() + RAND_MAX/5.0;
properties.Temperature  = rand() + RAND_MAX/5.0;
properties.FaultStatus = FALSE;

if (properties.Temperature > properties.TemperatureLimit && properties.FaultStatus == FALSE) {
        twInfoTable * faultData = 0;
        char msg[140];
        properties.FaultStatus = TRUE;
        sprintf(msg,"%s Temperature %2f exceeds threshold of %2f", 
            thingName, properties.Temperature, properties.TemperatureLimit);
        faultData = twInfoTable_CreateFromString("message", msg, TRUE);
        twApi_FireEvent(TW_THING, thingName, "SteamSensorFault", faultData, -1, TRUE);
        twInfoTable_Delete(faultData);
    }

 

Invoke Services

 

In order to invoke a Service, you will use the twApi_InvokeService function. The full documentation for this function can be found in [C SDK HOME DIR]/src/api/twApi.h. Refer to the table below for additional information.

 

ParameterTypeDescription
entityTypeInputThe type of Entity that the service belongs to. Enumeration values can be found in twDefinitions.h.
entityNameInputThe name of the Entity that the service belongs to.
serviceNameInputThe name of the Service to execute.
paramsInputA pointer to an Info Table containing the parameters to be passed into the Service. The calling function will retain ownership of this pointer and is responsible for cleaning up the memory after the call is complete.
resultInput/OutputA pointer to a twInfoTable pointer. In a successful request, this parameter will end up with a valid pointer to a twInfoTable that is the result of the Service invocation. The caller is responsible for deleting the returned primitive using twInfoTable_Delete. It is possible for the returned pointer to be NULL if an error occurred or no data is returned.
timeoutInputThe time (in milliseconds) to wait for a response from the server. A value of -1 uses the DEFAULT_MESSAGE_TIMEOUT as defined in twDefaultSettings.h.
forceConnectInputA Boolean value. If TRUE and the API is in the disconnected state of the duty cycle, the API will force a reconnect to send the request.

 

See below for an example in which the Copy service from the FileTransferSubsystem is called:

 

twDataShape * ds = NULL;
twInfoTable * it = NULL;
twInfoTableRow * row = NULL;
twInfoTable * transferInfo = NULL;
int res = 0;
const char * sourceRepo = "SimpleThing_1"; 
const char * sourcePath = "tw/hotfolder/";
const char * sourceFile = "source.txt";
const char * targetRepo = "SystemRepository";
const char * targetPath = "/";
const char * targetFile = "source.txt";
uint32_t timeout = 60;
char asynch = TRUE;
char * tid = 0;

/* Create an infotable out of the parameters */
ds = twDataShape_Create(twDataShapeEntry_Create("sourceRepo", NULL, TW_STRING));

res = twDataShape_AddEntry(ds, twDataShapeEntry_Create("sourcePath", NULL, TW_STRING));
res |= twDataShape_AddEntry(ds, twDataShapeEntry_Create("sourceFile", NULL, TW_STRING));
res |= twDataShape_AddEntry(ds, twDataShapeEntry_Create("targetRepo", NULL, TW_STRING));
res |= twDataShape_AddEntry(ds, twDataShapeEntry_Create("targetPath", NULL, TW_STRING));
res |= twDataShape_AddEntry(ds, twDataShapeEntry_Create("targetFile", NULL, TW_STRING));
res |= twDataShape_AddEntry(ds, twDataShapeEntry_Create("async", NULL, TW_BOOLEAN));
res |= twDataShape_AddEntry(ds, twDataShapeEntry_Create("timeout", NULL, TW_INTEGER));

it = twInfoTable_Create(ds); 

row = twInfoTableRow_Create(twPrimitive_CreateFromString(sourceRepo, TRUE));

res = twInfoTableRow_AddEntry(row, twPrimitive_CreateFromString(sourcePath, TRUE));
res |= twInfoTableRow_AddEntry(row, twPrimitive_CreateFromString(sourceFile, TRUE));
res |= twInfoTableRow_AddEntry(row, twPrimitive_CreateFromString(targetRepo, TRUE));
res |= twInfoTableRow_AddEntry(row, twPrimitive_CreateFromString(targetPath, TRUE));
res |= twInfoTableRow_AddEntry(row, twPrimitive_CreateFromString(targetFile, TRUE));
res |= twInfoTableRow_AddEntry(row, twPrimitive_CreateFromBoolean(asynch));
res |= twInfoTableRow_AddEntry(row, twPrimitive_CreateFromInteger(timeout));

twInfoTable_AddRow(it,row);
/* Make the service call */
res = twApi_InvokeService(TW_SUBSYSTEM, "FileTransferSubsystem", "Copy", it, &transferInfo, timeout ? (timeout * 2): -1, FALSE);
twInfoTable_Delete(it);

/* Grab the tid */
res = twInfoTable_GetString(transferInfo,"transferId",0, &tid);

 

Bind Event Handling

You may want to track exactly when your edge Entities are successfully bound to or unbound from the server. The reason for this is that only bound items should be interacting with the ThingWorx Platform and the ThingWorx Platform will never send any requests targeted at an Entity that is not bound. A simple example that only logs the bound Thing can be seen below. After creating this function, it will need to be registered using the twApi_RegisterBindEventCallback function before the connection is made.

 

void BindEventHandler(char * entityName, char isBound, void * userdata) {
    if (isBound) TW_LOG(TW_FORCE,"BindEventHandler: Entity %s was Bound", entityName);
    else TW_LOG(TW_FORCE,"BindEventHandler: Entity %s was Unbound", entityName);
}
….

twApi_RegisterBindEventCallback(thingName, BindEventHandler, NULL);

 

OnAuthenticated Event Handling

 

You may also want to know exactly when your Edge device has successfully authenticated and made a connection to the ThingWorx platform. Like the bind Event handling, this function will need to be made and registered. To register this handler, use the   twApi_RegisterOnAuthenticated Callback function before the connection is made. This handler form can also be used to do a delay bind for all Things.

 

void AuthEventHandler(char * credType, char * credValue, void * userdata) {
    if (!credType || !credValue) return;
    TW_LOG(TW_FORCE,"AuthEventHandler: Authenticated using %s = %s.  Userdata = 0x%x", credType, credValue, userdata);
    /* Could do a delayed bind here */
    /* twApi_BindThing(thingName); */
}

…

twApi_RegisterOnAuthenticatedCallback(AuthEventHandler, NULL);
 
 

Step 8: Tasks

 

If you are using the built-in Tasker to drive data collection or other types of repetitive or periodic activities, create a function for the task. Task functions are registered with the Tasker and then called at the rate specified after they are registered. The Tasker is a very simple, cooperative multitasker, so these functions should not take long to return and most certainly must not go into an infinite loop.

 

The signature for a task function is found in [C SDK HOME DIR]/src/utils/twTasker.h. The function is passed a DATETIME value with the current time and a void pointer that is passed into the Tasker when the task is registered. After creating this function, it will need to be registered using the twApi_CreateTask function after the connection is created. Below shows an example of creating this function, registering this function, and how this function can be used.

 

#define DATA_COLLECTION_RATE_MSEC 2000
void dataCollectionTask(DATETIME now, void * params) {
    /* TW_LOG(TW_TRACE,"dataCollectionTask: Executing"); */
    properties.TotalFlow = rand()/(RAND_MAX/10.0);
    properties.Pressure = 18 + rand()/(RAND_MAX/5.0);
    properties.Location.latitude = properties.Location.latitude + ((double)(rand() - RAND_MAX))/RAND_MAX/5;
    properties.Location.longitude = properties.Location.longitude + ((double)(rand() - RAND_MAX))/RAND_MAX/5;
    properties.Temperature  = 400 + rand()/(RAND_MAX/40);
    /* Check for a fault.  Only do something if we haven't already */
    if (properties.Temperature > properties.TemperatureLimit && properties.FaultStatus == FALSE) {
        twInfoTable * faultData = 0;
        char msg[140];
        properties.FaultStatus = TRUE;
        properties.InletValve = TRUE;
        sprintf(msg,"%s Temperature %2f exceeds threshold of %2f", 
            thingName, properties.Temperature, properties.TemperatureLimit);
        faultData = twInfoTable_CreateFromString("message", msg, TRUE);
        twApi_FireEvent(TW_THING, thingName, "SteamSensorFault", faultData, -1, TRUE);
        twInfoTable_Delete(faultData);
    }
    /* Update the properties on the server */
    sendPropertyUpdate();
}

…

twApi_CreateTask(DATA_COLLECTION_RATE_MSEC, dataCollectionTask);

…

while(1) {
        char in = 0;
#ifndef ENABLE_TASKER
        DATETIME now = twGetSystemTime(TRUE);
        twApi_TaskerFunction(now, NULL);
        twMessageHandler_msgHandlerTask(now, NULL);
        if (twTimeGreaterThan(now, nextDataCollectionTime)) {
            dataCollectionTask(now, NULL);
            nextDataCollectionTime = twAddMilliseconds(now, DATA_COLLECTION_RATE_MSEC);
        }
#else
        in = getch();
        if (in == 'q') break;
        else printf("\n");
#endif
        twSleepMsec(5);
    }

 

Click here to view Part 4 of this guide
Version history
Last update:
‎Mar 07, 2023 02:36 PM
Updated by:
Labels (2)
Contributors