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

Community Tip - Help us improve the PTC Community by taking this short Community Survey! X

IoT Tips

Sort by:
ThingWorx offers Docker based installations utilizing existing PostgreSQL databases. In newer releases ThingWorx Docker installers also offer using other databases.   Personally I'm using a certain method of deployment where I can just easily exchange some files, create new images and have a H2 based environment running for some quick tests.   As H2 is a built-in database, I will not dive into setting up the platform-settings.json for other connectivity. However other databases can be connected to by adjusting the platform-settings.json. This might also require an internal Docker Network structure which I will not elaborate on here.   Note: the following procedure is not fully supported as it's not using the deployment methods provided by the installers!   Create the Directory Structure   My Directory structure looks the following (expanded for the 8.2.x branch):   /home/ts/docker/ twx.8.0.x.h2 twx.8.1.x.h2 twx.8.2.x.h2 Dockerfile settings platform-settings.json <license_file> storage Thingworx.war twx.8.3.x.h2   I have a directory for every version I want to test with.   In each directory there's the Dockerfile - the recipe file I'm using. There's also the version specific Thingworx.war file as well as two directories: settings and storage which I will map to the ThingWorx directories inside the image later.   The Recipe File   FROM tomcat:latest MAINTAINER me@somewhere.com LABEL version = "8.2.0" LABEL database = "H2"  RUN mkdir -p /ThingworxPlatform RUN mkdir -p /ThingworxStorage RUN mkdir -p /ThingworxBackupStorage ENV LANG=C.UTF-8 ENV JAVA_OPTS="-server -d64 -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true -Dfile.encoding=UTF-8 -Duser.timezone=GMT -XX:+UseNUMA -XX:+UseG1GC -Djava.library.path=/usr/local/tomcat/webapps/Thingworx/WEB-INF/extensions COPY Thingworx.war /usr/local/tomcat/webapps VOLUME ["/ThingworxPlatform", "/ThingworxStorage"] EXPOSE 8080   I change the version label to keep track of the versions for each recipe.   Deploying   Build the Docker Image by navigating to the directory where the recipe file is based in   sudo docker build -t twx.8.2.x.h2 .   Create a Docker Container and start it   sudo docker run -d --name=twx.8.2.x.h2 -p 82:8080 -v /home/ts/docker/twx.8.2.x.h2/storage:/ThingworxStorage -v /home/ts/docker/twx.8.2.x.h2/settings:/ThingworxPlatform twx.8.2.x.h2   I change the name of the Image and the Container as well as the external port to distinguish all the different versions. The -v option maps the paths in my Operating System to the paths in the Docker Container, so I can browse the ThingworxStorage and ThingworxPlatform folder without connecting inside the Container. That's quite handy to check the logs, or place the license file.   Starting and Stopping   I can fire up and shut down Containers I need with the following commands:   sudo docker start twx.8.2.x.h2 sudo docker stop twx.8.2.x.h2   What next   That's just my basic setup. Usually I copy & paste a working directory for deploying another version and adjust what needs to be changed. You could use this as a basis for quick and easy deployment where even additional features could be added, i.e. HTTPS configuration or auto-deploying certain ThingWorx Extensions via a REST API call.   To ensure starting with a clean Image, for building new Images I delete the contents of the storage folder and only leave the platform-settings.json in the settings folder (I copy the license later after generating it with my new Device ID).
View full tip
Database backups are vital when it comes to ensure data integrity and data safety. PostgreSQL offers simple solutions to generate backups of the existing ThingWorx database instance and recover them when needed.   Please note that this does not replace a proper and well-defined disaster recovery plan. Export and Import are part of this strategy, but are not reflecting the complete strategy. The commands used in this post are for Windows, but can be adjusted to work on Linux-based systems as well.   Backup   To create a Backup, the export / dump functionality of PostgreSQL can be used.     pg_dump -U postgres -C thingworx > thingworxDump.sql     The -C option will include the statement to create the database in the .sql file and map it to the existing tablespace and user (e.g. 'thingworx' and 'twadmin'). The tablespace and user can be seen in the .sql file in the line with "ALTER DATABASE <dbname> OWNER TO <user>;"   In the above example, we're backing up the thingworx database schema and dump it into the thingworxDump.sql file   Tablespace, username & password are also included in the platform_settings.json   Restore   To restore the database, we just assume an empty PostgreSQL installation. We need to create the DB schema user first via the following commandline:     psql -U postgres -c "CREATE USER twadmin WITH PASSWORD 'ts';"     With the user created, we can now re-generate the tablespace and grant permissions to the twadmin user:   psql -U postgres -c "CREATE TABLESPACE thingworx OWNER twadmin LOCATION 'C:\ThingWorx\ThingworxPostgresqlStorage';" psql -U postgres -c "GRANT ALL PRIVILEGES ON TABLESPACE thingworx TO twadmin;" Finally the database itself can be restored by using the following commandline:   psql -U postgres < thingworxDump.sql This will create the database and populate it with tables, functions and sequences and will also restore the data from the .sql file.   It is important to have database, username and tablespace match with the original system - otherwise granting permissions and re-creating data might fail on recovery. User and tablespace can also be reused, so only the database has to be deleted before restoring it.   Part of the strategy   Part of the backup and recovery strategy should be to actually test the backup as well as the recovery part of the procedure. It should be well tested on a test-environment before deployed to any production environments. Take backups on a regular basis and test for disaster recovery once or twice a year, to ensure the procedure is still valid.   Data is the most important source in your application - protect it!  
View full tip
Original Post Date:            June 6, 2016   Description: This is a video tutorial on creating a Stream, adding a Data Shape with properties, and writing values to the Stream.    
View full tip
I have put together a small sample of how to get property values from a Windows Powershell command into Thingworx through an agent using the Java SDK. In order to use this you need to import entities from ExampleExport.xml and then run SteamSensorClient.java passing in the parameters shown in run-configuration.txt (URL, port and AppKey must be adapted for your system). ExampleExport.xml is a sample file distributed with the Java SDK which I’ve also included in the zipfile attached to this post. You need to go in Thingworx Composer to Import/Export … Import from File … Entities … Single File … Choose File … Import. Further instructions / details are given in this short video: Video Link : 2181
View full tip
Users of ThingWorx Analytics (TWA) may choose to create a predictive model using TWA or import a predictive model that was created using other software. When importing into or exporting out of TWA, this predictive model must be in a PMML (Predictive Model Markup Language) version 4.3+ format. This post describes how to complete the import and export processes. Exporting: The user may create a model in two main ways inside of TWA: using the Builder user interface, or by using ‘Create Job’ service that exists the Training Thing. Whichever method is used, a model Job Id is created automatically by TWA for that model. It is this model Job Id that is used to identify the model inside of TWA, regardless of what is being done with that model.   If a model is trained using Builder, the user may highlight that model, click ‘Job Details’, and then copy the Job ID. This is done as follows:   Next, the user will navigate to Browse --> Things --> …TrainingThing. This is the Training Microservice inside of TWA where all the functionality involved with training a model exists. Within the …TrainingThing, the user will execute the ‘RetrieveModel’ service under Services. When executing the service, the user will paste the model Job ID (ex. 49704f1a-7fcd-4e38-ab53-84ef46517d0a) they copied earlier, and press ‘Execute’. The resulting text can then be highlighted and copied to Notepad or some other text editor, and saved as .pmml format (ex. ‘ModelExport.pmml’).   Importing Through Results Microservice: To import a model that has been saved in PMML 4.3+ format into TWA using the Results Microservice, the user will navigate to Manage --> Repositories (ex. AnalyticsUploadStorage) --> Actions --> Upload, and choose the PMML file. The user will then navigate to Browse --> Things --> …ResultsThing. This is the Results Microservice inside of TWA where all the functionality exists related to previously trained models. Within the …ResultsThing, the user will execute the ‘UploadModel’ service under Services. Alternatively, the user can upload the model from any repository using ‘UploadModelFromRepository” service.   To create a model from the uploaded PMML inside of TWA, the user will fill out the filePath and name then execute the service. Note: This model will not show up in Builder, as that would require model validation information that is not part of the imported PMML file.   The resulting Job Id can be used to make predictions, such as by using the …PredictionThing’s BatchScore or RealtimeScore services. At this point, the uploaded model acts the same way as if the model were created inside of that TWA environment.       Importing Through Analytics Manager: To import a model that has been saved in PMML 4.3+ format into TWA using the Analytics Manager, the user will navigate to Analytics --> Analytics Manager --> Analysis Models, and click the green “New” button. Next the user will choose the provider name (or create a new one by navigating to Analytics --> Analytics Manager --> Analysis Providers). The user will also check the box to “Upload Model”, and click the grey “Choose File” button to find the PMML file. Finally, the user will click the black “Upload” button, then the green “Save” button.     At this point, the model is uploaded into ThingWorx Analytics, and the user may progress through the subsequent steps to set up “Analysis Events” and “Analysis Jobs” that will be powered by the imported model.
View full tip
Previously Installing & Connecting C SDK to Federated ThingWorx with VNC Tunneling to the Edge device Installing and configuring Web Socket Tunnel Extension on ThingWorx Platform Overview     Using the C SDK Edge client configuration we did earlier, we'll now create above illustrated setup. In this C SDK Client we'll push the data to ThingWorx Publisher with servername : TW802Neo to ThingWorx Subscriber servername : TW81. Notice that the SteamSensor2 on the pulisher server is the one binding to the C SDK client and the FederatedSteamThing on subscriber is only getting data from the SteamSensor2. Let's crack on!   Content   Configure ThingWorx to publish Configure ThingWorx to subscribe Publish entities from Publisher to the Subscriber Create Mashup to view data published to the subscriber Pre-requisite Minimum requirement is to have two ThingWorx servers running. Note that both ThingWorx systems can be publisher & subscriber at the same time.   Configure ThingWorx Publisher   Configuring Federation Subsystem   1. Navigate to ThingWorx Composer > Subsystems > Federation Subsystems and configure the following highlighted sections   Essentially its required to configure the Server Identification, i.e. My Server name (FQDN / IP) , My Server Description (optional) Federation subscribers this server publishes to, i.e. all the server you want to publish to from this server. Refer to the Federation Subsystem doc in the Help Center to check detail description on each configurable parameter.   2. Save the federation subsystem   Configuring a Thing to be published   1. Navigate back to the Composer home page and select the entity which you'd like to publish 2. In this case I'm using SteamSensor2 which is created to connect to the C SDK client 3. To publish edit the entity and click on Publish checkbox, like so 4. Save the entity   Configure ThingWorx Subscriber     Configuring Federation Subsystem   1. Navigate to ThingWorx Composer > Subsystems > Federation Subsystems and configure the following highlighted sections   Essentially its required to configure the Server Identification, i.e. My Server name (FQDN / IP) , My Server Description (optional) Refer to help center doc on Federation Subsystems should you need more detail on the configurable parameter If you only want to use this server as a subscriber of entities from the publishing ThingWorx Server, in that case you don't have to Configure the section Federation subscribers this server publishes to, I've configured here anyway to show that both servers can work as publishers and subscribers   2. Save the federation subsystem   Configuring a Thing to subscribe to a published Thing 1. Subscribing to an entity is fairly straight forward, I'll demonstrate by utilizing the C SDK client which is currently pushing values to my remote thing called SteamSensor2 on server https://tw802neo:443/Thingworx 2. I have already Published the StreamSensor2, see above section Configuring a Thing to be published 3. Create a Thing called FederatedStreamThing with RemoteThingWithTunnels as ThingTemplate, 4. Browser for the Identifier and select the required entity to which binding must be done, like so   5. Navigate to the Properties section for the entity, click Manage Bindings to search for the remote properties like so for adding them to this thing:     6. Save the entity and then we can see the values that were pushed from the client C SDK     7. Finally, we can also quickly see the values pulled via a Mashup created in the subscriber ThingWorx , below a is a simple mashup with grid widget pulling values using QueryPropertyHistory service  
View full tip
Original Post Date:            September 30, 2016   Description: This tutorial video will walk you through the installation process for the PostgreSQL-based version of the ThingWorx Platform (7.2) in a RHEL environment.  All required software components will be covered in this video.    
View full tip
This video is the 2 nd part of a series of 3 videos walking you through how to setup ThingWatcher for Anomaly Detection. In this video you will learn how to use “Discover UI” from the “New Composer” to bind simulated data coming through KEPServer for Anomaly Detection.   Updated Link for access to this video:  Anomaly Detection 8.0: Configuring Anomaly Alerts:  Part 2 of 3
View full tip
Video Author:                     Christophe Morfin Original Post Date:            October 2, 2017 Applicable Releases:        ThingWorx Analytics 8.1   Description:​ In this video we will walk thru the installation steps of ThingWorx Analytics Server 8.1.  This covers the Native Linux installation though the steps will be similar for a docker installation on Windows or Linux.    
View full tip
Video Author:                     Asia Garrouj Original Post Date:            June 13, 2017 Applicable Releases:        ThingWorx Analytics 8.0   Description: This video is the second of a 3 part series walking you through how to setup ThingWatcher for Anomaly Detection. In this second video you will learn how to use the "Discover UI" from the NextGen Composer to bind simulated data coming thru KEPServer for Anomaly Detection.    
View full tip
Hi,   I have just been playing around with the Edge MicroServer on a couple of Raspberry Pi's.  I obviously want them connected to ThingWorx, however they stop when the SSH session closes which isn't ideal.  I thought about doing something really quick and dirty using 'nohup', but this could have lead to many running processes, and wouldn't still not have started automatically when the Pi booted.  So, I did it right instead using init.d daemon configurations.   There are two files; one for the EMS and one for the Lua Script Resource.  You need to put these in /etc/init.d, then make sure they are owned by root.   sudo chown root /etc/init.d/thingworx* sudo chgrp root /etc/init.d/thingworx*   You'll need to modify the paths in the first lines of these files to match where you have your microserver folder.  Then you need to update the init.d daemon configs and enable the services.   sudo update-rc.d thingworx-ems defaults sudo update-rc.d thingworx-ems enable sudo update-rc.d thingworx-lsr defaults sudo update-rc.d thingworx-lsr enable   You can then start (stop, etc.) like this:   sudo service thingworx-ems start sudo service thingworx-lsr start   They should both also start automatically after a reboot.   Regards,   Greg
View full tip
In case it's useful for anyone, I successfully used the Thingworx Importer to import an Entity using version 8.4.1. The import command was in a CURL script (can also be run using e.g. Cygwin on Windows) and the entity data was contained in an XML file. The XML file is attached to this post, and the CURL script is copied into the bottom of the post body.   Notes on using the script: Copy CURL code into import.sh You might need to change the line endings to UNIX, e.g. in Notepad++ menu option Edit > EOL Conversion > Unix Give permissions to run import.sh (chmod +x import.sh) ./import.sh >>>>>>>>>>>> #!/bin/bash APPKEY="2e6704c0-XXXX-XXXX-XXXX-a1589e387d1a" TWX_HTTP_PORT="8018" FILE_PATH_TWX="Things_testImport.xml" PROTOCOL="http://" IP="localhost" URL="$PROTOCOL""$IP"":""$TWX_HTTP_PORT""/Thingworx/Importer?purpose=import" curl -X POST -H 'appKey: '$APPKEY \ -H 'Content-Type: multipart/form-data' \ -H 'Accept: application/json' \ -H 'x-thingworx-session: true' \ -H 'X-XSRF-TOKEN: TWX-XSRF-TOKEN-VALUE' \ -F 'upload=@'$FILE_PATH_TWX \ $URL <<<<<<<<<<<<   Also TWX Importer is explained in this support article.
View full tip
This Javascript snippet creates a random value between those limits:   var dbl_Value = Math.floor(Math.random()*(max-min+1)+min);
View full tip
This Expert Session will provide you with an in depth explanation behind how Signals are calculated in ThingWorx Analytics, what purpose they serve, and why we use them.  Some basic mathematical concepts are discussed so viewers will have a better idea of how ThingWorx Analytics operates behind the scenes.     For full-sized viewing, click on the YouTube link in the player controls.   Visit the Online Success Guide to access our Expert Session videos at any time as well as additional information about ThingWorx training and services.
View full tip
Requirements: 6.1.2+ Geofences are geometric shapes drawn virtually on a geographical area that represents a fence that can be crossed by a device.  The Axeda Platform has built-in support for mobile locations and geofences, which can be linked to the rules engine to enable notifications based on geofence crossing. What this tutorial covers This tutorial demonstrates the workflow of creating a geofence through to creating the expression rules with notifications, then how the mobile location can trigger the rules. 1) Creating the Geofence 2) Creating the Expression Rule There is currently no user interface built into the Axeda Applications Console which interacts with geofences.  For a sample application with a geofence user interface, see Sample Project: Traxeda​ (TODO).  For a single Custom Object that includes all of the functionality described below, see the end of  this document. The properties of a geofence are a name, a description, and a series of coordinates based on Well-Known Text (WKT) syntax (see the OpenGIS Simple Features Specification). def addGeofence(CONTEXT, map){     Geofence myGeofence = new Geofence(CONTEXT)        myGeofence.name = map.name     if(map.type != "polygon" && map.type != "circle")     {         throw new Exception("Invalid type: need 'polygon' or 'circle', not '$map.type'")     }     else if(map.type == "polygon")     {         def geo = map.locs.loc.inject( "POLYGON (("){ str, item ->             def lng = item.lng             def lat = item.lat             str += "$lng $lat,"         str         }         //the first location also has to be the last location         myGeofence.geometry = geo + map.locs.loc[0].lng + " " + map.locs.loc[0].lat + "))"         //Something like this is built:         //POLYGON ((-71.082118 42.383892,-70.867198 42.540923,-71.203654 42.495374,-71.284678 42.349394,-71.163829 42.221382,-71.003154 42.266114,-71.082118 42.383892))     }     else if(map.type == "circle")     {         def lng = map.locs.loc[0].lng         def lat = map.locs.loc[0].lat         myGeofence.geometry = "POINT ($lng $lat)"         //POINT (-71.082118 42.383892)         myGeofence.buffer = map.radius.toDouble()     }     myGeofence.description = "ALERT:::$map.alertType:::$map.alert"     try {          myGeofence.store()     }     catch (e){         logger.info e.localizedMessage             return null     }     myGeofence } The geofence itself does not interact with devices in any way.  Rather it is the Expression Rule that is applied to models and devices and that invokes the geofence when a mobile location is passed in. Creating the Expression Rule The Expression Rule for the Geofence is built as follows: TYPE: MobileLocation IF:  Expression set to "InNamedGeofence" for entering and "!InNamedGeofence" for exiting. The following function creates this expression rule: /* Sample call createGeofenceExpressionRule(CONTEXT, "My Geofence", "rule_MyGeofence", "in", "You entered the geofence!", "SDK Generated Geofence Rule", 100) */ def createGeofenceExpressionRule(com.axeda.drm.sdk.Context CONTEXT, String geofencename, String rulename, String alertType, String alertMessage, String ruledescription, int severity){     ExpressionRuleFinder erf = new ExpressionRuleFinder(CONTEXT)     erf.setName(rulename)     ExpressionRule expressionRule1 = erf.findOne()     expressionRule1?.delete()        def expressionRule = new ExpressionRule(CONTEXT)     expressionRule.setName(rulename)     expressionRule.setDescription(ruledescription)     expressionRule.setTriggerName("MobileLocation")     def ifExpStr = "InNamedGeofence(\"$geofencename\", Location.location)"     if(alertType == "out"){         ifExpStr = "!" + ifExpStr     }     expressionRule.setIfExpression(new Expression(ifExpStr))     expressionRule.setThenExpression(new Expression("CreateAlarm(\"$alertMessage\", severity)"))     expressionRule.setEnabled(true)     expressionRule.setConsecutive(false)     expressionRule.store()     expressionRule } Then the rule associations must be created to apply the rule to a model or device. /* Sample call findOrCreateRuleAssociations(CONTEXT, myModel, expressionRule, "EXPRESSION_RULE", "MODEL") Where expressionRule is the rule created in the above example */ def findOrCreateRuleAssociations(Context CONTEXT, Object entity, Object rule, String ruleType, String entityType){     // rule type is whether this is an expression rule     ruleType = ruleType ?: "EXPRESSION_RULE"     entityType = entityType ?: "DEVICE_INCLUDE"     RuleAssociationFinder ruleAssociationFinder = new RuleAssociationFinder(CONTEXT)     ruleAssociationFinder.setRuleId(rule.id.value)     ruleAssociationFinder.setRuleType(RuleType.valueOf(ruleType))     ruleAssociationFinder.setEntityId(entity.id.value)     ruleAssociationFinder.setEntityType(EntityType.valueOf(entityType))     def ruleAssociations = ruleAssociationFinder.findAll()     if (!ruleAssociations || ruleAssociations?.size() == 0){         def ruleAssociation = new RuleAssociation(CONTEXT)         ruleAssociation.entityId = entity.id.value         ruleAssociation.entityType = EntityType.valueOf(entityType)         ruleAssociation.ruleType = RuleType.valueOf(ruleType)         ruleAssociation.setRuleId(rule.id.value)         ruleAssociation.store()         ruleAssociations = [ruleAssociation]     }     return ruleAssociations } The rule will now be triggered when any device of the applied model sends a mobile location within the geofence, which in turn will create an alarm. Here is a custom object with the complete geofence functionality: import com.axeda.drm.sdk.Context import com.axeda.drm.sdk.geofence.Geofence import com.axeda.drm.sdk.geofence.GeofenceFinder import com.axeda.drm.sdk.rules.engine.Expression import com.axeda.drm.sdk.rules.engine.ExpressionRule import com.axeda.drm.sdk.rules.engine.ExpressionRuleFinder import com.axeda.drm.sdk.rules.engine.RuleAssociation import com.axeda.drm.sdk.rules.engine.RuleAssociationFinder import com.axeda.drm.sdk.rules.engine.RuleType import com.axeda.drm.sdk.common.EntityType import com.axeda.drm.sdk.device.Model import com.axeda.drm.sdk.device.ModelFinder try {     def Context CONTEXT = Context.getSDKContext()     def model = findOrCreateModel(CONTEXT, "FooModel")     def sampleCircle = [         "name": "My Circle",         "alert": "My Geofence Alert Text",         "type": "circle",         "alertType": "in",         "radius": "65.76",         "locs": [             [                 "loc": [   "lat": "42.60970621339408",   "lng": "-73.201904296875"   ]             ]         ]     ]     def samplePolygon = [         "name": "My Polygon",         "alert": "My Geofence Alert Text",         "type": "polygon",         "alertType": "out",         "locs": [             ["loc": [  "lng": -71.2604999542236,  "lat": 42.3384903145478  ]],             ["loc": [  "lng": -71.4218616485596,  "lat": 42.3242772020001  ]],             ["loc": [  "lng": -71.5585041046143,  "lat": 42.2653600946699  ]],             ["loc": [  "lng": -71.5413379669189,  "lat": 42.1885837119108  ]],             ["loc": [  "lng": -71.4719867706299,  "lat": 42.1137514551207  ]],             ["loc": [  "lng": -71.3737964630127,  "lat": 42.0398506628541  ]],             ["loc": [  "lng": -71.2508869171143,  "lat": 42.0311807962068  ]],             ["loc": [  "lng": -71.1355304718018,  "lat": 42.2084223174036  ]],             ["loc": [  "lng": -71.2604999542236,  "lat": 42.3384903145478  ]]         ]     ]     // find geofence if it exists     def circle = findGeofenceByName(CONTEXT, sampleCircle.name)     // create circular geofence     if (!circle){         circle = addGeofence(CONTEXT, sampleCircle)     }     // create rule for circular geofence     def circleRule = createGeofenceExpressionRule(CONTEXT, circle.name, "${circle.name}__Rule",                                                                            sampleCircle.alertType, sampleCircle.alert, "SDK Generated Geofence Rule", 100)     // apply rule to new Model     findOrCreateRuleAssociations(CONTEXT, model, circleRule, "EXPRESSION_RULE", "MODEL")     def polygon = findGeofenceByName(CONTEXT, samplePolygon.name)     if (!polygon){         polygon = addGeofence(CONTEXT, samplePolygon)     }     def polygonRule = createGeofenceExpressionRule(CONTEXT, polygon.name, "${polygon.name}__Rule",                                                                               samplePolygon.alertType, samplePolygon.alert, "SDK Generated Geofence Rule", 100)     // apply rule to new Model     findOrCreateRuleAssociations(CONTEXT, model, polygonRule, "EXPRESSION_RULE", "MODEL") } catch (Exception e) {     logger.info(e.localizedMessage) } return true def findGeofenceByName(CONTEXT, name){     GeofenceFinder geofenceFinder = new GeofenceFinder(CONTEXT)     geofenceFinder.setName(name)     def geofence = geofenceFinder.find()     geofence } def addGeofence(CONTEXT, map){     Geofence myGeofence = new Geofence(CONTEXT)     myGeofence.name = map.name     if(map.type != "polygon" && map.type != "circle") {         throw new Exception("Invalid type: need 'polygon' or 'circle', not '$map.type'")     } else if(map.type == "polygon") {         def geo = map.locs.loc.inject( "POLYGON (("){ str, item ->             def lng = item.lng             def lat = item.lat             str += "$lng $lat,"             str         }         //the first location also has to be the last location         myGeofence.geometry = geo + map.locs.loc[0].lng + " " + map.locs.loc[0].lat + "))"         //Something like this is built:         //POLYGON ((-71.082118 42.383892,-70.867198 42.540923,-71.203654 42.495374,-71.284678 42.349394,-71.163829 42.221382,-71.003154  42.266114,-71.082118 42.383892))     } else if(map.type == "circle") {         def lng = map.locs.loc[0].lng         def lat = map.locs.loc[0].lat         myGeofence.geometry = "POINT ($lng $lat)"         //POINT (-71.082118 42.383892)         myGeofence.buffer = map.radius.toDouble()     }     myGeofence.description = "ALERT:::$map.alertType:::$map.alert"     try {         myGeofence.store()     }  catch (e) {         logger.info e.localizedMessage         return null     }     myGeofence } def createGeofenceExpressionRule(com.axeda.drm.sdk.Context CONTEXT, String geofencename, String rulename,                                                      String alertType, String alertMessage, String ruledescription, int severity) {     ExpressionRuleFinder erf = new ExpressionRuleFinder(CONTEXT)     erf.setName(rulename)     ExpressionRule expressionRule1 = erf.findOne()     expressionRule1?.delete()     def expressionRule = new ExpressionRule(CONTEXT)     expressionRule.setName(rulename)     expressionRule.setDescription(ruledescription)     expressionRule.setTriggerName("MobileLocation")     def ifExpStr = "InNamedGeofence(\"$geofencename\", Location.location)"     if(alertType == "out"){         ifExpStr = "!" + ifExpStr     }     expressionRule.setIfExpression(new Expression(ifExpStr))     expressionRule.setThenExpression(new Expression("CreateAlarm(\"$alertMessage\", severity)"))     expressionRule.setEnabled(true)     expressionRule.setConsecutive(false)     expressionRule.store()     expressionRule } def findOrCreateRuleAssociations(Context CONTEXT, Object entity, Object rule, String ruleType, String entityType) {     // rule type is whether this is an expression rule     ruleType = ruleType ?: "EXPRESSION_RULE"     entityType = entityType ?: "DEVICE_INCLUDE"     RuleAssociationFinder ruleAssociationFinder = new RuleAssociationFinder(CONTEXT)     ruleAssociationFinder.setRuleId(rule.id.value)     ruleAssociationFinder.setRuleType(RuleType.valueOf(ruleType))     ruleAssociationFinder.setEntityId(entity.id.value)     ruleAssociationFinder.setEntityType(EntityType.valueOf(entityType))     def ruleAssociations = ruleAssociationFinder.findAll()     if (!ruleAssociations || ruleAssociations?.size() == 0){         def ruleAssociation = new RuleAssociation(CONTEXT)         ruleAssociation.entityId = entity.id.value         ruleAssociation.entityType = EntityType.valueOf(entityType)         ruleAssociation.ruleType = RuleType.valueOf(ruleType)         ruleAssociation.setRuleId(rule.id.value)         ruleAssociation.store()         ruleAssociations = [ruleAssociation]     }     return ruleAssociations } def findOrCreateModel(Context CONTEXT, String modelName) {     ModelFinder modelFinder = new ModelFinder(CONTEXT)     modelFinder.setName(modelName)     def model = modelFinder.find()     if (!model){         model = new Model(CONTEXT, modelName);         model.store();     }     return model } https://gist.github.com/axeda/6529288/raw/5ffca58c3c48256b81287d6a6f2d2db63cd5cd2b/AddGeofence.groovy
View full tip
Warning This post is quite long, has various chapters and you might get bored reading it. If you just want a summary read the "Use case" and "Conclusion" chapter - and maybe the "Required Logic" chapter, because I made a cool graph for it. The rest is all about implementation... Introduction I recently had the opportunity to deliver a ThingWorx training for Saint Gobain. One of the use cases for their ThingWorx application is monitoring machine errors and outages on the production line. If an outage or error status is triggered, the machine operator will see a popup on the monitoring screen where he is forced to select a root cause. This root cause will then be persisted in ThingWorx for more data transformation, analytics and reporting - like cost analysis or optimization opportunities. During the training we were also discussing on how such a forced root cause monitoring can be implemented via Mashups and the usage of modal popups. I've compiled the details into this post as it might also interest other developers. The ThingWorx Entities I'm using in this example can be downloaded from here Note: I'm using the word "Alert" here - but not in the context of a ThingWorx Property Alert... just beware to not be confused due to the wording. Use Case One of the requirements for Saint Gobain's IoT Solution was an interactive alert monitoring directly in the factory on the production machines. Let's say the machine has stopped, the root cause should be recorded. For this an interactive popup will be displayed on the machine's monitoring display and an employee has to choose the root cause from a pre-defined list. This could be planned outages, e.g. for maintenance or unplanned outages, e.g. material jam. The root cause will then be recorded and a history of outage causes can be stored in a ThingWorx value stream. This can then be later analyzed with e.g. ThingWorx Analytics capabilities to understand and optimize the machine's production capabilities and efficiency. As the root cause must be entered, the popup will be forced to be displayed when a certain condition / criteria is met - and it will only disappar when a root cause is chosen. The user should not be able to interact with any other elements of the Mashup and not be able to just close the popup. The popup will close itself and reset the initial condition once the root cause has been identified and chosen. Requirements Required Entities Required Logic Note: Just to make it easier to manage and export Entities, I will add all of the created elements in a new Project called RootCausePopups. All of the elements will have a "rcp_" added in front of their name - just to make it easier for me to find and identify them. Implementation Before we start - set a Project Context Create Entities Create a Popup Mashup Create the Main Mashup Testing the Mashups Conclusion Certain conditions (like the state of a checkbox) can be used to trigger modal popups. A modal popup forces a user interaction and the interaction will not offer any other option until a choice is made. With these parameters it's easy to have mandatory reaction from users when it's important to capture data which rely on the analysis of an engineer or a user - e.g. reasons for machine outages. Using this technique there's not much training required for staff, other than pushing a button with an option of their choice - this saves quite some time in capturing data in any other way (e.g. updating Excel files or manual pen-and-paper techniques). As this data is now part of the ThingWorx instance it can be used for further transformation, analysis or just for monitoring purposes There's of course more possibilities when it comes to states and formatting which would exhaust the context of this post - but feel free to explore... In the example we wouldn't need the textbox, but it's there to demonstrate if the correct values are persisted or not In the example we could of course also set the visibility of the checkbox to false, so that we would only see the popup and the Grid holding historic information We could also create different StateDefinitions to color-format / text-format the input differently from the output in the Grid If you found this interesting (and actually made it to the end of this post) - feel free to play with this concept a bit more... The dependencies might seem a bit difficult, but it should be easy to implement and to adjust to your own ideas and requirements.
View full tip
Contents: Introduction Prerequisites Installing Java Installing PostgreSQL Running the Installer Post Installation Steps Troubleshooting tips   Introduction:   Starting with ThingWorx 8.4, PTC released a new way to install a fresh ThingWorx environment.  This installer takes care of all the permissions, database scripts, credential encryption, and tomcat options that previously needed to be done manually.  More information on the installer can be found in the ThingWorx Help Center   NOTE: This is different than the Docker installer we have available in earlier releases.   As of right now, the installation guide has very basic instructions for the installer.  The purpose of this post is to show you from start to finish what the process looks like.  For this example, I chose to deploy PostgreSQL 10 on the local system to keep things simple.   Prerequisites:   Download the latest Java 8 SE JDK RPM for RHEL Get your database ready: If you're accessing a remote PostgreSQL instance, make sure PSQL is installed and working on your ThingWorx Server Download the appropriate installer from support.ptc.com Ensure the RHEL user that will be executing the installer has SUDO privileges   NOTE: There are pieces of the manual installation guide that I had to reference in order to get JAVA and PostgreSQL properly configured.   Installing Java:   Per Page 83, I downloaded the latest Linux x64 RPM for Java 8 SE JDK (201) and followed steps 2-8 to configure Java. For step 5, I needed to use the -f parameter listed in the guide under NOTE Step 7 make sure you don't accidentally select OpenJDK if it was preinstalled   Installing PostgreSQL:   I'm following along with the Version 10 download instructions found on https://www.postgresql.org/download/linux/redhat/ NOTE: this needs root access, so run all the commands with SUDO Install the client packages Postgresql10 I will Install the optional server packages postgresql10-server since this is a local PostgreSQL instance Complete step 7 to enable automatic start.  We need to set the postgres password so our ThingWorx installer is able to create our thingworx user and the database.  This can be done with the following command: NOTE: Since this is the master user for your database, it is highly recommended to use a password that has a combination of case, numbers, letters, and symbols Sudo passwd postgres Although, this may be redundant, I also run the following command to update the password used in PostgreSQL : sudo -u postgres psql -c "ALTER ROLE postgres WITH password '<password from above>'" Navigate to /var/lib/pgsql/10/data and open pg_hba.conf for editing Review page 91 of the Installation guide to determine which setting best applies to your business needs In the same directory open postgresql.conf Scroll down to "listen_addresses" line and un-comment it.  This would  be the place to make changes if you expect remote connections to access the database.  If it is local, then the default of localhost is fine Restart PostgreSQL to apply these changes: Sudo service postgresql-10 restart   Running the Installer:   Everything should be in place now to run our installer.  Extract the ThingWorxFoundationPostgres-1.2.0-SNAPSHOT.run file to the ~ (home) directory Execute the .run file: NOTE: If it doesn't let you execute the file, it may not have extracted as an executable.  Run the below command to make it executable then try again: Chmod -x ThingWorxFoundationPostgres-1.2.0-SNAPSHOT.run Sudo ./ThingWorxFoundationPostgres-1.2.0-SNAPSHOT.run   At this point you'll be going through text to setup your installation settings.  I'll briefly list out the order you'll see them below: Terms and conditions and whether you agree Where you want ThingWorx deployed (/opt by default) NOTE: this folder will contain ThingworxStorage/ ThingworxPlatform/ tomcat/ etc… Installation Configuration user (twxfoundation by default).  This step creates a user in RHEL that will have ownership of Tomcat, various ThingWorx directory's, etc ThingWorx Administrator Password.  Used to login to ThingWorx Composer. WRITE THIS DOWN SOMEWHERE!  You cannot retrieve this password, and most likely will require you to do a fresh installation if you forget it Tomcat Port http (8080) Tomcat SSL port (8443) Use SSL For simplicity, I chose not to use it for this exercise PostgreSQL information Host Name : mine is local, so localhost Port (5432) Administrator Username (Administrator) : use postgres here, since that's the DB user password we updated above Admin password : use the postgres password ThingWorx Database login username (twadmin).  This user will be created in PostgreSQL and be tied to our ThingWorx database ThingWorx database login password: NOTE There's no place to re-enter your password, so make sure you write this down.   Unexpected issue:   For this particular install, I kept running into a failure saying "Warning: Failed to validate the PostgreSQL connection.  Check the information you entered".  I opened another putty connection and, as root, navigated to /var/lib/pgsql/10/data/log and opened the postgresql log to find the following:   2019-02-28 17:10:30.678 UTC [93377] LOG:  could not connect to Ident server at address "::1", port 113: Connection refused 2019-02-28 17:10:30.678 UTC [93377] FATAL:  Ident authentication failed for user "postgres" 2019-02-28 17:10:30.678 UTC [93377] DETAIL:  Connection matched pg_hba.conf line 84: "host    all             all             ::1/128                 ident"     The solution for me was to go into the pg_hba.conf and change the IPv6 local connections from ident to md5.  Again, make sure you are reading through the PostgreSQL documentation and adjusting these properties in a way that meets both your security and business needs.   Once the change was made, I restarted postgresql, and switched back over to my Putty instance that had the installer going.     A summary pops up for a few items, and then it asks if you're ready to continue NOTE: The progress bar goes to 100% pretty quickly, and doesn't appear to move.  Just let it sit for a few minutes while it finishes up Copy the Thingworx Device ID for future reference To check if ThingWorx is running, run 'sudo service Thingworx-Foundation status' in your command line If it is active (running) try to access it with a remote browser: More information around the command Firewalld can be found here  http://<thingworxurl>:<tomcatport>/Thingworx NOTE: If it just hangs, check your firewall to make sure the port is open for external communication   Post Installation Steps:   Licensing: Navigate to /opt/ThingWorxPostgres-1.2.0-SNAPSHOT/licensingconfigurator and run the twx-licensing-configurator.run as SUDO Choose whether or not you want PTC to store your credentials and download the license for you, or if you want to manually download the license yourself from http://support.ptc.com -> Manage Licenses (bottom right) For this example, I manually downloaded the license Move the license file over to the ThingWorx Server Since you're running the licensingconfigurator as SUDO, don't put this file into your user's home directory.  Instead, put it into /tmp NOTE: Change the downloaded filename to license_capability_response.bin.  Otherwise the file will not be recognized Then it will ask for your ThingWorx Administrator password This appears to be used for verification after the license is in place, and it sees if it can successfully log into your system Once it has completed, and assuming it says "Setup has finished configuration licensing for ThingWorx", open up a web browser and login as Administrator -> Monitor -> Subsystems -> Licensing Subsystem and verify that your licensing information looks correct on the system   Extensions: Extra security has been added as of 8.4 around importing Extensions.  More details can be found in the Help Center In short, adding extensions is disabled by default, and you need to add some lines into your /ThingworxPlatform/platform-settings.json under the "PlatformSettingsConfig" section. For example, here is what I added:    "PlatformSettingsConfig": {                 "BasicSettings": {                         "BackupStorage": "/opt/ThingWorxPostgres-1.2.0-SNAPSHOT/ThingworxBackupStorage",                         "DatabaseLogRetentionPolicy": 7,                         "EnableBackup": true,                         "EnableHA": false,                         "EnableSystemLogging": true,                         "HTTPRequestHeaderMaxLength": 2000,                         "HTTPRequestParameterMaxLength": 2000,                         "InternalAesCryptographicKeyLength": 128,                         "Storage": "/opt/ThingWorxPostgres-1.2.0-SNAPSHOT/ThingworxStorage"                 },                 "ExtensionPackageImportPolicy": {                        "importEnabled": true,                        "allowJarResources": true,                        "allowJavascriptResources": false,                        "allowCSSResources": false,                        "allowJSONResources": false,                        "allowWebAppResources": false,                        "allowEntities": true,                        "allowExtensibleEntities": false       }           }   Make sure you set the appropriate items above to true based on what your extensions require   Troubleshooting:   If things backfire, depending on where you are in the setup process, the following logs should be looked at for clues on the failure:   Installation: /tmp/bitrock_installer.logs I believe the installation directory (default /opt/ThingWorxPostgres-1.2.0-SNAPSHOT) will contain a log file if the installer fails /opt/ThingWorxPostgres-1.2.0-SNAPSHOT/ThingworxStorage/logs/ (need root access) /opt/ThingWorxPostgres-1.2.0-SNAPSHOT/tomcat/apache-tomcat-<version>/logs PostgreSQL (requires root): /var/lib/pgsql/10/data/log LicensingConfigurator : /opt/ThingWorxPostgres-1.2.0-SNAPSHOT/licensingconfigurator
View full tip
Video Author:                     Durgesh Patel Original Post Date:            June 12, 2017 Applicable Releases:        ThingWorx   Description: In this video we review the different features available in Grid Advanced Extension version 2.0.  
View full tip
Video Author:                      Asia Garrouj Original Post Date:            June 13, 2017 Applicable Releases:        ThingWorx Analytics 8.0   Description: This video is the first of a 3 part series walking you through how to setup ThingWatcher for Anomaly Detection. In this first video you will learn the basics of how to establish connectivity between KEPServer and the ThingWorx Platform.    
View full tip
Disclaimer: This post does of course not express any political views.   Pie Chart Coloring   In ThingWorx Pie Charts use a default color schema based on the DefaultChartStyle Definitions. These schemas are using fixed numbering and coloring systems, e.g. 1 is blue, 2 is green, 3 is red and so on. All Pie Charts will be rendered with these colors in the same order, no matter which data the chart is using. Visualization of data with the default colors might not necessarily help in creating an easy to read chart.   Just take a look at the following example with the default color schema. Let's just take political parties - as they are usually associated with a distinct color - to illustrate how the default color schema will fail depending on the data displayed.         In the first example, just by sheer coincidence the colors are perfectly matching the parties. When introducing a new party to the pool suddenly the blues are rendered green and the yellows rendered light-blue etc. This can be quite confusing, especially on election night 😉   Custom Color Schema   PoliticalParties Thing   To test a custom color schema, we first need to create a new Thing: PoliticalParties as a GenericThing Add a dataset property with the following PoliticalParties DataShape.         Save the Thing and set the InfoTable to:   Key Value The Purples 20 The Blues 20 The Greens 20 The Reds 20 The Yellows 20 Others 20   Number values don't actually matter too much, as the Pie Chart will automatically distribute them according to their percentage.   PoliticalParties Mashup   Create a new Mashup and add a PieChart to the canvas. Bind the PoliticalParties > GetPropertyValues > dataset to the Data input of the Widget. Ensure to set the LabelField to key and the ValueField to value for a correct mapping.     Save the Mashup and preview it.   It should show a non-matching color for each party listed in the InfoTable.   Custom Styles and States   Create new custom Style Definitions for each political party. As the Pie Chart is only using the Background Color other properties can stay on the default. I chose to go with a more muted version of the colors to make the chart easier to look at.         With the newly defined colors we can now generate a new State Definition as follows:       The States allow to evaluate the key-Strings in the Thing's InfoTable and assign a Style Definition depending on the actual value. In this definition we map a color schema based on the InfoTable's key-value to create a 1:1 mapping for the Strings.   This means, no matter where a certain party is positioned in the chart it will be tinted with its associated color.   Refining the Mashup   Back in the Mashup, select the PieChart. In the ColorFormat property choose the newly created State Definition.     Save the Mashup and preview it. With the States and Styles applies, colors are now displayed correctly.       Even when changing positions and numbers in the original InfoTable of the PoliticalParties Thing, the chart now considers the mapping of Strings and still displays the colors correctly.  
View full tip
Requirements:  Axeda 6.1.6+ The Axeda Applications User Interface can be extended to accommodate varying degrees of customization.  This ability to customize the base product enables repurposing the Axeda Applications User Interface to serve a specific audience. What this tutorial covers This tutorial discusses three ways to extend the Axeda Applications User Interface, which can be achieved via the following features: Customizing the Look and Feel - Use your own custom stylesheet to replace the default page styles, even on a per-user basis Extended UI Modules - Insert your own Extended UI Module into the Service > Asset Dashboard Custom Tab - Create a custom tab that loads content from a custom M2M application Customizing the Look and Feel of the Axeda Applications User Interface You can add style changes into a user.css file which you then upload like any other custom application, via the Administration > Extensions tab as a zip archive.  Make sure to adhere to the expected directory structure and follow the naming convention for the zip archive. Images - store image files in a directory called <userName>/images Styles - store user.css and any style sheet(s) that it imports in a directory called <userName>/styles Documentation - store documentation files in a directory called <userName>/doc. The naming convention is to name the archive by the username of the user who should be able to see the changes, i.e. jsmith is the username so jsmith.zip is the archive name. For step-by-step instructions for customizing the UI, Axeda customers may refer to http://<<yourdomain>>.axeda.com/help/en/stylesheets_for_user_branding.htm andhttp://<<yourdomain>>.axeda.com/help/en/upload_user_branding.htm . Extended UI Modules Extended UI Modules can be added to the Asset Dashboard to provide custom content alongside the default modules.  The modules can contain the output of a custom object or a custom application, all within the context of the particular asset being viewed. Create the Extended UI Content Option 1: an Extended UI Type Custom Object Navigate to Configuration > New > Custom Object This Custom Object should output HTML with any Javascript and/or CSS styling embedded inline.  Parameters may be defined here and made available to the script as "parameters.label". Example: def iframehtml = """<html>   <head>     <script type='text/javascript' src='https://www.google.com/jsapi'></script>     <script type='text/javascript'>       google.load('visualization', '1', {packages:['gauge']});       google.setOnLoadCallback(drawChart);       function drawChart() {         var data = new google.visualization.DataTable();         data.addColumn('string', 'Label');         data.addColumn('number', 'Value');         data.addRows([           ['$parameters.label', $parameters.value]         ]);         var options = {           redFrom: 90, redTo: 100,           yellowFrom:75, yellowTo: 90,           minorTicks: 5         };         var chart = new google.visualization.Gauge(document.getElementById('chart_div'));         chart.draw(data, options);       }     </script>   </head>   <body style="background: white;">     <div id='chart_div'></div>   </body> </html>​ """ return ['Content-Type': 'text/html', 'Content': iframehtml.toString()]      Option 2: A Custom Application Create a zip file that contains an index html file at the root of the directory, any stylesheets, scripts and images you prefer and upload the zip as a Custom Application (see the example zip file included at the end of this article). Navigate to Administration > Extensions .  Enter the information for the zip file and upload. Create the Extended UI Object Option 1: Using the Axeda Applications Console Navigate to Configuration > New > Extended UI Module Note that the parameters are entered in URI format  myvalue=mykey&othervalue=otherkey If Content Source is set to Custom Application rather than Custom Object, the Custom Applications will become available as the Extended UI Module content. Option 2: Use Axeda Artisan Check out Developing with Axeda Artisan in order to make use of this method.  Add the Extended UI Module to the apc-metadata.xml and it will be created for you automatically on Maven upload.  Note that Artisan does not support Model Preferences, so you will still have to add the module through the Axeda UI as described below. <extendedUIModule>     <!-- you can create the module here, but you still have to use the Axeda Console to apply it to the model where the module should show up -->     <title>extendedUI_name</title>     <height>180</height>     <source>         <type>CUSTOM_APPLICATION</type>         <name>customapp_name</name>     </source> </extendedUIModule> Add the Extended UI Module to the Model Preferences Navigate to Configuration > View > Model and click Preferences under UI Configuration next to the model that should display the Extended UI Module for its assets. Click Asset Dashboard Layout Select the Extended UI Module from the left and click the arrow to add it to the desired column.  The asterisks indicate Extended UI Modules, as opposed to default modules. Click Submit and navigate to an Asset Dashboard to see the module displayed. Now you have an Extended UI Module with your custom content. Custom Tabs Upload a custom application as a custom tab. And there you have it. For Artisan developers, to enable a custom application as a custom tab, insert the following into the apc-metadata.xml: <application>     <description>string</description>     <applicationId>string</applicationId>     <indexFile>string</indexFile>     <zipFile>relative path from apc-metadata.xml to the zip file containing the application files</zipFile>     <customTab>         <tabPrivilegeName>the privilege name required for the tab to be shown</tabPrivilegeName>         <afterTab>the name of the tab after which to place this tab</afterTab>         <showFooter>[true|false]</showFooter>         <tabNames>             <label>                 <locale>the i18n locale (for example en_US or ja_JP)</locale>                 <name>the name to be displayed for the locale</name>             </label>         </tabNames>     </customTab> </application>      Authentication within Extended UI Components When working with Custom Applications in custom tabs or modules, the user session ID is made available through a special variable that you can access from the landing page (such as index.html) only: %%SESSIONID_TOKEN%%      This variable is substituted directly for the session id, which makes the authentication for viewing the Extended UI component appear seamless to the end user. In order to make this ID available for AJAX calls, the index.html file should store the session ID as it is initializing.  Additionally, index.html should instruct the browser not to cache the page, or the session ID may mistakenly be used to authenticate after it expires. In index.html: <html>     <head>         <title>My Custom App</title> <META HTTP-EQUIV="CACHE-CONTROL" CONTENT="NO-CACHE"> <link media="screen" href="styles/axeda.css" rel="stylesheet" type="text/css"/> <script src="scripts/jquery-1.9.0.min.js" type="text/javascript"></script> <script type="text/javascript">             $(window).load(function () {                 App.init(encodeURIComponent("%%SESSIONID_TOKEN%%"));             })         </script>     </head>    In App.js: App.init = function (sessionID) {         // put initial processing here         storeSessionId( sessionID )         App.callScriptoWithStoredSessionID()     }    That's it!  You can now customize the look and feel of the Axeda Applications Console, as well as add an Extended UI Module and a Custom Tab. Further Reading Developing with Axeda Artisan Axeda Sample Application: Populating A Web Page with Data Items Common Questions I want to display my custom app on a custom tab. How should I manage authentication within my custom tab app? Answer:  Use Javascript to store the session ID injected as a variable into the index.html page, then use that to authenticate Scripto calls to the Axeda Platform. Are there example programs to get started? Answer:  There are several examples of Artisan projects to get started Axeda Sample Application: Populating A Web Page with Data Items An Axeda instance - https://<customerInstance>.axeda.com/artisan
View full tip
Announcements