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

Low Level Device Connection Part 2

No ratings

 

 

Step 5: Properties

 

In the Delivery Truck application, there are three Delivery Truck Things. Each Thing has a number of Properties based on its location, speed, and its deliveries carried out. In this design, when a delivery is made or the truck is no longer moving, the Property values are updated. The deliveryTruck.c helper C file is based on the DeliveryTruck Entities in the Composer. After calling the construct function, there are a number of steps necessary to get going. For the SimpleThing application, there are a number of methods for creating Properties, Events, Services, and Data Shapes for ease of use.

 

Properties can be created in the client or just registered and utilized. In the SimpleThingClient application, Properties are created. In the DeliveryTruckClient application, Properties are bound to their ThingWorx Platform counterpart. Two types of structures are used by the C SDK to define Properties when it is seen fit to do so and can be found in [C SDK HOME DIR]/src/api/twProperties.h:

 

 Name                   Structure            Description
Property DefinitionstwPropertyDefDescribes the basic information for the Properties that will be available to ThingWorx and can be added to a client application.
Property ValuestwPropertyAssociates the Property name with a value, timestamp, and quality.

 

NOTE: The C SDK provides a number of Macros located in [C SDK HOME DIR]/src/api/twMacros.h. This guide will use these Macros while providing input on the use of pure function calls.

 

The Macro example below can be found in the main source file for the SimpleThingClient application and the accompanying helper file simple_thing.c.

 

TW_PROPERTY("TempProperty", "Description for TempProperty", TW_NUMBER);
    TW_ADD_BOOLEAN_ASPECT("TempProperty", TW_ASPECT_ISREADONLY,TRUE);
    TW_ADD_BOOLEAN_ASPECT("TempProperty", TW_ASPECT_ISLOGGED,TRUE);

 

NOTE: The list of aspect configurations can be seen in [C SDK HOME DIR]/src/api/twConstants.h. Property values can be set with defaults using the aspects setting. Setting a default value in the client will affect the Property in the ThingWorx platform after binding. It will not set a local value in the client application.

 

For the DeliveryTruckClient, we registered, read, and update Properties without using the Property definitions. Which method of using Properties is based on the application being built.

 

NOTE: Updating Properties in the ThingWorx Platform while the application is running, will update the values in the client application. To update the values in the platform to match, end the Property read section of your property handler function with a function to set the platform value.

 

The createTruckThing function for the deliveryTruck.c source code takes a truck name as a parameter and is used to register the Properties, functions, and handlers for each truck.

 

The updateTruckThing function for the deliveryTruck.c source code takes a truck name as a parameter and is used to either initialize a struct for DeliveryTruck Properties, or simulate a truck stop Event, update Properties, then fire an Event for the ThingWorx platform.

 

Connecting properties to be used on the platform is as easy as registering the property and optionally adding aspects. The following shows the properties that correlate to those in the DeliveryTruck entities in the Composer. To do this within the code, you would use the TW_PROPERTY macro as shown in the deliveryTruck.c. This macro must be proceeded by either TW_DECLARE_SHAPE, TW_DECLARE_TEMPLATE or TW_MAKE_THING because these macros declare variables used by the TW_PROPERTY that follow them.

 

//TW_PROPERTY(propertyName,description,type)

TW_PROPERTY(PROPERTY_NAME_DRIVER, NO_DESCRIPTION, TW_STRING);
TW_PROPERTY(PROPERTY_NAME_DELIVERIES_LEFT, NO_DESCRIPTION, TW_NUMBER);
TW_PROPERTY(PROPERTY_NAME_TOTAL_DELIVERIES, NO_DESCRIPTION, TW_NUMBER);
TW_PROPERTY(PROPERTY_NAME_DELIVERIES_MADE, NO_DESCRIPTION, TW_NUMBER);
TW_PROPERTY(PROPERTY_NAME_LOCATION, NO_DESCRIPTION, TW_LOCATION);
TW_PROPERTY(PROPERTY_NAME_SPEED, NO_DESCRIPTION, "TW_NUMBER);

 

Read Properties

 

Reading Properties from a ThingWorx platform Thing or the returned Properties of a Service can be done using the TW_GET_PROPERTY macro. Examples of its use can be seen in all of the provided applications. An example can be seen below:

 

int flow = TW_GET_PROPERTY(thingName, "TotalFlow").number;
int pressue = TW_GET_PROPERTY(thingName, "Pressure").number;
twLocation location = TW_GET_PROPERTY(thingName, "Location").location;
int temperature = TW_GET_PROPERTY(thingName, "Temperature").number;

 

Write Properties

 

Writing Properties to a ThingWorx platform Thing from a variable storing is value uses a similarly named method. Using the TW_SET_PROPERTY macro will be able to send values to the platform. Examples of its use can be seen in all of the provided applications. An example is shown below:

 

TW_SET_PROPERTY(thingName, "TotalFlow", TW_MAKE_NUMBER(rand() / (RAND_MAX / 10.0)));
TW_SET_PROPERTY(thingName, "Pressure", TW_MAKE_NUMBER(18 + rand() / (RAND_MAX / 5.0)));
TW_SET_PROPERTY(thingName, "Location", TW_MAKE_LOC(gpsroute[location_step].latitude,gpsroute[location_step].longitude,gpsroute[location_step].elevation));

 

This macro utilizes the twApi_PushSubscribedProperties function call to push all property updates to the server. This can be seen in the updateTruckThing function in deliveryTruck.c.

 

Property Change Listeners

 

Using the Observer pattern, you can take advantage of the Property change listener functionality. With this pattern, you create functions that will be notified when a value of a Property has been changed (whether on the server or locally by your program when the TW_SET_PROPERTY macro is called).

 

Add a Property Change Listener

 

In order to add a Property change listener, call the twExt_AddPropertyChangeListener function using the:

 

  • Name of the Thing (entityName)
  • Property this listener should watch
  • Function that will be called when the property has changed

 

void simplePropertyObserver(const char * entityName, const char * thingName,twPrimitive* newValue){
    printf("My Value has changed\n");
}

void test_simplePropertyChangeListener() {
    {
        TW_MAKE_THING("observedThing",TW_THING_TEMPLATE_GENERIC);
        TW_PROPERTY("TotalFlow", TW_NO_DESCRIPTION, TW_NUMBER);
    }
    
    twExt_AddPropertyChangeListener("observedThing",TW_OBSERVE_ALL_PROPERTIES,simplePropertyObserver);
    TW_SET_PROPERTY("observedThing","TotalFlow",TW_MAKE_NUMBER(50));
}

NOTE: Setting the propertyName parameter to NULL or TW_OBSERVE_ALL_PROPERTIES, the function specified by the propertyChangeListenerFunction parameter will be used for ALL properties.

 

Remove a Property Change Listener

 

In order to release the memory for your application when done with utilizing listeners for the Property, call the twExt_RemovePropertyChangeListener function.

 

void simplePropertyObserver(const char * entityName, const char * thingName,twPrimitive* newValue){
    printf("My Value has changed\n");
}

twExt_RemovePropertyChangeListener(simplePropertyObserver);
 
 
 

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.

 

 Parameter         Type                  Description
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_RegisterOnAuthenticatedCallback 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);

 

 

Click here to view Part 3 of this guide.

Version history
Last update:
‎Mar 07, 2023 02:22 PM
Updated by:
Labels (1)
Contributors