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

Community Tip - You can Bookmark boards, posts or articles that you'd like to access again easily! X

IoT Tips

Sort by:
Starting with ThingWorx Manufacturing Apps 8.1, a Connection Info box was added to Controls Advisor that will display the Configuration settings needed in KEPServerEX. In Controls Advisor, select the server instance you are connecting with, then select the Key icon Be sure the information displayed here matches the settings you have in place in your KEPServerEX Project Properties in the ThingWorx property group.    
View full tip
/* Define a DataShape used in an InfoTable Parameter for this service call */ twDataShape* sampleInfoTableAsParameterDs = twDataShape_Create(twDataShapeEntry_Create("ColumnA",NO_DESCRIPTION,TW_STRING)); twDataShape_AddEntry(sampleInfoTableAsParameterDs,twDataShapeEntry_Create("ColumnB",NO_DESCRIPTION,TW_NUMBER)); twDataShape_AddEntry(sampleInfoTableAsParameterDs,twDataShapeEntry_Create("ColumnC",NO_DESCRIPTION,TW_BOOLEAN)); twDataShape_SetName(sampleInfoTableAsParameterDs,"SampleInfoTableAsParameterDataShape");      /* Define Input Parameter that is an InfoTable of Shape SampleInfoTableAsParameterDataShape */ twDataShapeEntry* infoTableDsEntry = twDataShapeEntry_Create("itParam",NULL,TW_INFOTABLE); twDataShapeEntry_AddAspect(infoTableDsEntry, "dataShape", twPrimitive_CreateFromString("SampleInfoTableAsParameterDataShape", TRUE));    twDataShape* inputParametersDefinitionDs = twDataShape_Create(infoTableDsEntry);   /* Register remote function */ twApi_RegisterService(TW_THING, SERVICE_INTEGRATION_THINGNAME, "testMultiRowInfotable", NO_DESCRIPTION,   inputParametersDefinitionDs, TW_NOTHING, NULL, PlatformCallsServiceWithMultiRowInfoTableServiceImpl, NULL); /* Note that you will have to manually create the datashape in ThingWorx before attempting to add this remote service to your Thing. */
View full tip
In this blog I will be inspecting the setup below, using the REST APIs exposed by the different components (log-analysis-free guaranteed).   The components are started in stages, and I will do some exploration between each stages : EMS only EMS + LSR 1 EMS + LSR 1 + LSR 2    The REST APIs   Edge MicroServer (EMS) REST API This API is very similar to the ThingWorx platform REST API, see REST APIs Supported by WS EMS for specificity. I will be monitoring the EMS using the LocalEms virtual Thing (EMS only) ThingWorx Platform REST API This is the well known ThingWorx Core REST API -  see REST API Core Concepts I will be monitoring the EMS, from the platform, using an EMSGateway thing. The EMSGateway exposes on the platform some of the LocalEms services (like GetEdgeThings). I'm also inspecting the remote properties / services / events exposed on the things using the RemoteThing::GetRemoteMetadata service. Lua Script Resource (LSR) REST API This API largely differs from the ones above, the API documentation is served by the LSR itself (e.g. http://localhost:8001/)   I will use it to list the scripts loaded by the LSR process.  Configuration   See above diagram - Platform is listening on port 8084, no encryption, EMS and LSRs on the same host...   Platform configuration : for each edge thing, a corresponding remote thing was manually created on the platform EMSGateway1 as a EMSGateway Thing Template EMSOnlyThing as a RemoteThingWithFileTransfer LUAThing2 as a RemoteThing LUAThing1 as a RemoteThing LUAOnlyThing as a RemoteThing   EMS configuration : /etc/config.json - listening on default port 8000   {     "ws_servers": [{             "host": "tws74neo",             "port": 8084         }     ],     "appKey": "xxxxxx-5417-4248-bc01-yyyyyyy",     "logger": {         "level": "TRACE"     },     "ws_connection": {         "encryption": "none"     },     "auto_bind": [{             "name": "EMSGateway1",             "gateway": true         }, {             "name": "EMSOnlyThing",             "gateway": false         }, {             "name": "LUAThing2",             "host": "localhost",             "port": 8002,             "gateway": false         }, {             "name": "LUAThing1",             "gateway": false         }     ],     "file": {         "virtual_dirs": [{                 "emsrepository": "E:\\ptc\\ThingWorx\\EMS-5-3-2\\repositories\\data"             }         ],         "staging_dir": "E:\\ptc\\ThingWorx\\EMS-5-3-2\\repositories\\staging"     } } LSR process (1) : /etc/config.lua - listening on default port 8001 (using the out of the box sample Lua scripts)   scripts.log_level = "INFO"   scripts.LUAThing1 = {     file = "thing.lua",     template = "example", }   scripts.sample = {   file = "sample.lua" } LSR process (2) : /etc/config2.lua - listening on port 8002 (using the out of the box sample Lua scripts) This LSR process is started with command "luaScriptResource.exe -cfg .\etc\config2.lua"   scripts.log_level = "INFO" scripts.script_resource_port = 8002 scripts.LUAThing2 = {     file = "thing.lua",     template = "example", }   scripts.LUAOnlyThing = {     file = "thing.lua",     template = "example", }   Stage 1 : EMS only     ThingWorx REST API   Request: Call the GetEdgeThings service on the EMSGateway1 thing POST  twx74neo:8084/Thingworx/Things/EMSGateway1/Services/GetEdgeThings Response: As expected, only the remote things flagged as auto_bind are listed   name host port path keepalive timeout proto user accept EMSGateway1   8001.0 / 60000.0 30000.0 http   application/json EMSOnlyThing   8001.0 / 60000.0 30000.0 http   application/json LUAThing1   8001.0 / 60000.0 30000.0 http   application/json LUAThing2 localhost 8002.0 / 60000.0 30000.0 http   application/json                                                               Request: Call the GetRemoteMetadata service on the LUAThing1 thing POST  twx74neo:8084/Thingworx/Things/LUAThing1/Services/GetRemoteMetadata Response: As expected, remote properties / services and events are not available since the LSR associated with this thing is off.   Unable to Invoke Service GetRemoteMetadata on LUAThing1 : null   EMS REST API   Request: Call the GetEdgeThings service on the LocalEms virtual thing POST  localhost:8000/Thingworx/Things/LocalEms/Services/GetEdgeThings Response: output is identical to the gateway thing on the platform   { "name": "EMSGateway1", "host": "", "port": 8001, "path": "/",  "keepalive": 60000, "timeout": 30000, "proto": "http", "user": "", "accept": "application/json" }, { "name": "EMSOnlyThing", "host": "", "port": 8001, "path": "/", "keepalive": 60000, "timeout": 30000, "proto": "http", "user": "", "accept": "application/json" }, { "name": "LUAThing1", "host": "", "port": 8001, "path": "/", "keepalive": 60000, "timeout": 30000, "proto": "http", "user": "", "accept": "application/json" }, { "name": "LUAThing2", "host": "localhost", "port": 8002, "path": "/", "keepalive": 60000, "timeout": 30000, "proto": "http", "user": "", "accept": "application/json"}            LSR REST API N/A - no LSR process started yet.   Stage 2 : EMS + LSR 1 (8001)     ThingWorx REST API   Request: Call the GetEdgeThings service on the EMSGateway1 thing POST  twx74neo:8084/Thingworx/Things/EMSGateway1/Services/GetEdgeThings Response: LUAThing1 is associated to an LUA script   name host port path keepalive timeout proto user accept EMSGateway1   8001.0 / 60000.0 30000.0 http   application/json EMSOnlyThing   8001.0 / 60000.0 30000.0 http   application/json LUAThing1 localhost 8001.0 /scripts/Thingworx 60000.0 15000.0 http   application/json LUAThing2 localhost 8002.0 / 60000.0 30000.0 http   application/json   Request: Call the GetRemoteMetadata service on the LUAThing1 thing POST  twx74neo:8084/Thingworx/Things/LUAThing1/Services/GetRemoteMetadata Response: Now that the Lua script for LUAThing1 is running, remote properties / services and events are available   {"isSystemObject":false,"propertyDefinitions":{"Script_Pushed_Datetime":{"sourceType":"ThingShape","aspects":{"isReadOnly":false,"dataChangeThreshold":0,"defaultValue":1495619610000,"isPersistent":false,"pushThreshold":0,"dataChangeType":"VALUE","cacheTime":0,"pushType":"ALWAYS"},"name":"Script_Pushed_Datetime","description":"","category":"","tags":[],"baseType":"DATETIME","ordinal":0},"Pushed_InMemory_Boolean":{"sourceType":"ThingShape","aspects":....   EMS REST API      LocalEms::GetEdgeThings returns same output as EMSGateway::GetEdgeThings   LSR REST API (port 8001)   Request: List all the scripts running in the first LSR GET  localhost:8001/scripts?format=text/html Response: We find our sample script and the script associated with LUAThing1 (the Thingworx script is part of the infrastructure and always there)   Name Status Result File LUAThing1 Running   E:\ptc\ThingWorx\EMS-5-3-2\etc\thingworx\scripts\thing.lua sample Running   sample.lua Thingworx Running   E:\ptc\ThingWorx\EMS-5-3-2\etc\thingworx\scripts\thingworx.lu   Stage 3 : EMS + LSR 1 (8001) + LSR 2 (8002)     ThingWorx REST API   Request: Call the GetEdgeThings service on the EMSGateway1 thing POST  twx74neo:8084/Thingworx/Things/EMSGateway1/Services/GetEdgeThings Response: LUAOnlyThing is now listed and LUAThing2 is associated with a LUA script   name host port path keepalive timeout proto user accept EMSGateway1   8001.0 / 60000.0 30000.0 http   application/json EMSOnlyThing   8001.0 / 60000.0 30000.0 http   application/json LUAOnlyThing localhost 8002.0 /scripts/Thingworx 60000.0 15000.0 http   application/json LUAThing1 localhost 8001.0 /scripts/Thingworx 60000.0 15000.0 http   application/json LUAThing2 localhost 8002.0 /scripts/Thingworx 60000.0 15000.0 http   application/json   EMS REST API      LocalEms::GetEdgeThings returns same output as  EMSGateway::GetEdgeThings   LSR REST API (port 8002)   Request: List all the scripts running in the second LSR GET  localhost:8002/scripts?format=text/html Response: Returns the status of all the scripts currently loaded   Name Status Result File LUAOnlyThing Running   .\etc\thingworx\scripts\thing.lua LUAThing2 Running   .\etc\thingworx\scripts\thing.lua Thingworx Running   .\etc\thingworx\scripts\thingworx.lua
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
This video will walk you through the first steps of how to set-up Analytics Manager for Real-Time Scoring. More specifically this video demonstrates how to create an Analysis Provider and start ThingPredictor Agent. NOTE: For version 8.1 the startup command for the Agent has changed view the command in PTC Help center.   Updated Link for access to this video:  ThingWorx Analytics Manager: Create an Analysis Provider & Start the ThingPredictor Agent                                  
View full tip
One of the issues we have encountered recently is the fact that we could not establish a VNC Remote session. The edge was located outside of the internal network where the Tomcat was hosted, and all access to the instance was through an Apache reverse proxy. The EMS was able to connect successfully to the Server, because the Apache had correctly setup the Websocket forwarding through the following directive: ProxyPass "/Thingworx/WS/"  "wss://192.168.0.2/Thingworx/WS" However, we saw that tunnels immediately closed after creation and as a result (or so we thought), we could not connect from the HTML5 VNC viewer. More diagnostics revealed that you need to have ProxyPass directives for the following: -the EMS will make calls to another WS endpoint, called WSTunnelServer. After you setup this, the EMS will be able to create tunnels to the server. -the HTML5 VNC page will make a "websocket" call to yet another WS endpoint, called WSTunnelClient. Only at this step you have the ability to successfully use tunnels through a reverse proxy. Hope it helps!
View full tip
When we do connections to JDBC connected databases, as much as possible we want to leverage the Database Server side capabilities to do the querying, aggregating and everything else for us. So if we need to do filtering or math add it to the query statement so it is done server side, by the Database server not the Thingworx runtime server. Besides that there is much more that can be done, since databases are powerful (all the good stuff like joins, union, distinct, generated and calculated fields and what not) The more we can do Database server side the better it is for the Thingworx runtime performance. Now everyone hopefully knows the [[  ]] parameter substitution. So we can easily build an SQL service that has several input parameters, it will look like: Select * from table where item1=[[par1]] AND item2 =[[par2]] etc. But we can take this up a notch with the super powerful yet super dangerous <<   >> Now we can do a service that just says <<sqlQuery>> and use another service to build something like: select * from table where item1 in “val1,val2,val3” etc. If you can avoid it, only use [[  ]] but if needed there always is <<   >> but you must make sure that you properly secure that service with the system user and validate the service that invokes this service, since <<  >> is vulnerable to SQL Injection
View full tip
Attached to this article is my slide deck from Liveworx 2016 titled "Getting Mobile with ThingWorx and Android". It is an overview of the ThingWorx android SDK which had its 1.0 release this past April. Also attached is the Light Blue Bean bluetooth integration example I demoed at the show.
View full tip
Objective Learn how the Scripto Web Service helps you to present platform information in your HTML with JavaScript and dynamic page updating.  After this tutorial, you will know how to create Axeda Custom Objects that return formatted results to JavaScript using XmlHttpResponse, and how a very simple page can incorporate platform data into your browser-based user interface. Part 1 - Simple Scripto In Using Scripto, you learned how Scripto can be called from very simple clients, even the most basic HTTP tools. This tutorial builds on the examples in that tutorial. The following HelloWorld script accepts a parameter named "foo". This means that the caller of the script may supply a value for this parameter, and the script simple returns a message that includes the value supplied. import static com.axeda.sdk.v2.dsl.Bridges.* import com.axeda.services.v2.* import com.axeda.sdk.v2.exception.* return "Hello world, ${parameters.foo}" In the first part of this tutorial, we'll be creating an HTML page with some JavaScript that simply calls the HelloWorld script and puts the result on the page. Create an HTML File Open up your favorite text editor and create a blank document. Paste in this simple scaffold, which includes a very simple FORM with fields for your developer platform email and password, and the "foo" parameter. <html> <head> <title>Axeda Developer Connection Simple Ajax HelloWorld Example</title> </head> <body> <form name="f1">         Platform email (login): <input name="username" type="text"><br/>         Password: <input name="password" type="password"><br/>         foo: <input name="foo" type="text"><br/> <input value="Go" type="button" onclick='JavaScript: callScripto()'/></p> <div id="result"></div> </form> </body> </html> Pretty basic HTML that you've seen lots of times. Notice the form onclick refers to a JavaScript function. We'll be adding that next. Add the JavaScript Directly under the <title> tag, add the following <script language="Javascript"> var scriptoURL ="http://dev6.axeda.com/services/v1/rest/Scripto/execute/"; var scriptName ="HelloWorld2"; </script> This defines our JavaScript block, and a couple of constants to tell our script where the server's Scripto REST endpoint is, and the name of the script we will be running. Let's add in our callScripto() function. Paste the following directly under the scriptName variable declaration: function callScripto(){ try{                 netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead"); }catch(e){ // must be IE }    var xmlHttpReq =false;    var self =this;    // Mozilla/Safari    if(window.XMLHttpRequest){                 self.xmlHttpReq =new XMLHttpRequest();    }// IE elseif(window.ActiveXObject){                 self.xmlHttpReq =new ActiveXObject("Microsoft.XMLHTTP"); }    var form = document.forms['f1'];    var username = form.username.value;    var password = form.password.value;    var url = scriptoURL + scriptName +"?username="+ username +"&password="+ password;             self.xmlHttpReq.open('POST', url,true);             self.xmlHttpReq.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');             self.xmlHttpReq.onreadystatechange =function() {       if(self.xmlHttpReq.readyState ==4){                     updatepage(self.xmlHttpReq.responseText);       }    }    var foo = form.foo.value;    var qstr ='foo='+escape(foo);    self.xmlHttpReq.send(qstr); } That was a lot to process in one chunk, so let's examine each piece. This piece just tells the browser that we'll be wanting to make some Ajax calls to a remote server. We'll be running the example right off a local file system (at first), so this is necessary to ask for permission. try{                 netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead"); }catch(e){ // must be IE } This part creates an XmlHttpRequest object, which is a standard object available in browsers via JavaScript. Because of slight browser differences, this code creates the correct object based on the browser type. var xmlHttpReq =false; var self =this; // Mozilla/Safari if(window.XMLHttpRequest){                 self.xmlHttpReq =new XMLHttpRequest(); } // IE elseif(window.ActiveXObject){                 self.xmlHttpReq =new ActiveXObject("Microsoft.XMLHTTP"); } Next we create the URL that will be used to make the HTTP call. This simply combines our scriptoURL, scriptName, and platform credentials. var form = document.forms['f1']; var username = form.username.value; var password = form.password.value; var url = scriptoURL + scriptName +"?username="+ username +"&password="+ password; Now let's tell the xmlHttpReq object what we want from it. we'll also reference the name of another JavaScript function which will be invoked when the operation completes. self.xmlHttpReq.open('POST', url,true);     self.xmlHttpReq.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');     self.xmlHttpReq.onreadystatechange =function(){ if(self.xmlHttpReq.readyState ==4){             updatepage(self.xmlHttpReq.responseText); } } Finally, for this function, we'll grab the "foo" parameter from the form and tell the prepped xmlHttpReq object to post it. var qstr ='foo='+escape(foo);     self.xmlHttpReq.send(qstr); almost done. We just need to supply the updatepage function that we referenced above. Add this code directly before the </script> close tag: function updatepage(str){             document.getElementById("result").innerHTML = str; } Try it out Save your file as helloworld.html and open it in a browser by starting your browser and choosing "Open File". You can also download a zip with the file prepared for you at the end of this page. If you are using Internet Explorer, IE will pop a bar asking you if it is OK for the script inside this page to execute a script. Choose "Allow Blocked Content". Type in your platform email address (the address you registered for the developer connection with) and your password. Enter any text that you like for "foo". When you click "Go", the area below the button will display the result of the Scripto call. Note that if you are using Mozilla Firefox, you will be warned about the script wanting to access a remote server. Click "Allow". Congratulations! You have learned how to call a Custom Object-backed Scripto service to display dynamic platform content inside a very simple HTML page. Next Steps Be sure to check out the tutorial on Hosting Custom Applications to learn how you can make this page get directly served from your platform account, with its very own URL. Also explore code samples that show more sophisticated HTML+AJAX examples using Google Charts and other presentation tools.
View full tip
Adaptive Machine Messaging Protocol The Adaptive Machine Messaging Protocol (AMMP) is a simple, byte-efficient, lightweight messaging protocol used to facilitate Internet of Things (IoT) communications and to build IoT connectivity into your product. Using a RESTful API, AMMP provides a semantic structure for IoT information exchange and leverages HTTPS as the means for sending and receiving messages between an edge device and the Axeda® Machine Cloud®. AMMP uses JavaScript Object Notation (JSON) allowing any device that is capable of making an HTTP transmission to interact with the Axeda Platform. Utilizing a common network transport that is friendly to local network proxies and firewalls, and at the same time using JSON for a compact, human-readable, language-independent, and easily constructed data representation, AMMP simplifies device communication and reduces the work needed to connect to the Axeda Machine Cloud. For complete information about the Adaptive Machine Messaging Protocol, refer to the Adaptive Machine Messaging Protocol (AMMP) Technical Reference. AMMP Toolkits The AMMP Toolkits are libraries that allows you to connect your devices to the Axeda Platform using AMMP.  The AMMP Toolkits support transmission of data, alarms, events, locations; error handling and reporting; as well as exchanging files with the Axeda Platform. AMMP Android-Based Toolkit The AMMP Android-Based Toolkit library conforms to the AMMP Protocol Version 1.1. AMMP Android Toolkit AMMP Android Toolkit Developers Reference AMMP Protocol v1.1 Technical Reference AMMP Java-Based ToolkitThe AMMP Java-Based Toolkit library conforms to the AMMP Protocol Version 1.1.  AMMP Java Toolkit AMMP Java Toolkit Developers Reference AMMP Protocol v1.1 Technical Reference AMMP C-Based ToolkitThe AMMP C-Based Toolkit library conforms to the AMMP Protocol Version 1.1. AMMP C Toolkit AMMP C Toolkit Developers Reference AMMP Protocol v1.1 Technical Reference The above resources may be found at the PTC Support Portal.
View full tip
Scripto provides a RESTful endpoint for Groovy Custom Objects on the Axeda Platform.  Custom Objects exposed via Scripto can be accessed via a GET or a POST, and the script will have access to request parameters or body contents. Any Custom Object of the "Action" type will automatically be exposed via Scripto. The URL for a Scripto service is currently defined by the name of the Custom Object: GET: http://{{YourHostName}}/services/v1/rest/Scripto/execute/<customObjectName> Scripto enables the creation of "Domain Specific Services". This allows implementers to take the Axeda Domain Objects (Assets, Models, DataItems, Alarms) and expose them via a service that models the real-world domain directly (trucks, ATMs, MRI Machines, sensor readings). This is especially useful when creating a domain-specific UI, or when integrating with another application that will push or pull data. Authentication There are several ways to test your Scripto scripts, as well as several different authentication methods. The following authentication methods can be used: Request Parameter credentials: ?username=<yourUserName>&password=<yourPassword> Request Parameter sessionId (retrieved from the Auth service): ?sessionid=<sessionId> Basic Authentication (challenge): From a browser or CURL, simply browse to the URL to receive an HTTP Basic challenge. Request Parameters You can access the parameters to the Groovy script via two Objects, Call and Request. Request is actually just a sub-class of Call, so the values will always be the same regardless of which Object you use.  Although parameters may be accessed off of either object, Call is preferable when Chaining Custom Objects (TODO LINK) together.  Call also includes a reference to the logger which can be used to log debug messages. GET:  http://{{YourHostName}}/services/v1/rest/Scripto/execute/<Your Script Name>?sessionid=<Session Id>&serial_number=mySerialNumber Accessing Parameters through the Request Object import com.axeda.drm.sdk.scripto.Request // Request.parameters is a map of strings def serial_number = Request.parameters.serial_number assert serial_number == "mySerialNumber"       Accessing Parameters through the Call Object import com.axeda.drm.sdk.customobject.Call // Call.parameters is a map of strings def serial_number = Call.parameters.serial_number assert serial_number == "mySerialNumber"       Accessing the POST Body through the Request Object The content from a POST request to Scripto is accessible as a string via the body field in the Request object.  Use Slurpers for XML or JSON to parse it into an object. POST:  http://{{YourHostName}}/services/v1/rest/Scripto/execute/<Your Script Name>?sessionid=<Session Id> Response: { "serial_number":"mySerialNumber"} import com.axeda.drm.sdk.scripto.Request def body = Request.body def slurper = new JsonSlurper() def result = slurper.parseText(body) assert result.serial_number == "mySerialNumber"       Returning Plain Text Groovy custom objects must return some content.  The format of that content is flexible and can be returned as plain text, JSON, XML, or even binary files. The follow example simply returns plain text. GET:  http://{{YourHostName}}/services/v1/rest/Scripto/execute/<Your Script Name> // Outputs:  hello return ["Content-Type":"text/plain","Content":"hello"]       Returning JSON We use the JSONObject Class to format our Map-based content into a JSON structure. The eliminates the need for any concern around formatting, you just build up Maps of Maps and it will be properly formatted by the fromObject() utility method. GET:  http://{{YourHostName}}/services/v1/rest/Scripto/execute/<Your Script Name> import net.sf.json.JSONObject root = [   items:[    num_1: “one”,    num_2: “two”            ] ] /** Outputs {   "items": {  "num_1": "one", "num_2": "two"  } } **/ return ['Content-Type': 'application/json', 'Content': JSONObject.fromObject(root).toString(2)]       Link to JSONObject documentation Returning XML To return XML, we use the MarkupBuilder to build the XML response. This allows us to create code that follows the format of the XML that is being generated. GET:  http://{{YourHostName}}/services/v1/rest/Scripto/execute/<Your Script Name>?sessionid=<Session Id> import groovy.xml.MarkupBuilder def writer = new StringWriter() def xml = new MarkupBuilder(writer) xml.root(){     items(){         num_1("one")         num_2("two")     } } /** Outputs <root>   <items>     <num_1>one</num_1>     <num_2>two</num_2>   </items> </root> **/ return ['Content-Type': 'text/xml', 'Content': writer.toString()]       Link to Groovy MarkupBuilder documentation Returning Binary Content To return binary content, you typically will use the fileStore API to upload a file that you can then download using Scripto.  See the fileInfo section to learn more. In this example we connect the InputStream which is associated with the getFileData() method directly to the output of the Scripto script. This will cause the bytes available in the stream to be directly forwarded to the client as the body of the response. GET:  http://{{Your Host Name}}/services/v1/rest/Scripto/execute/{{Your Script Name}}?sessionid={{Session Id}}&fileId=123 import static com.axeda.sdk.v2.dsl.Bridges.* import com.axeda.services.v2.* import com.axeda.sdk.v2.exception.* def contentType = parameters.type ?: 'image/jpg' return ['Content':fileInfoBridge.getFileData(parameters.fileId), 'Content-Type':contentType]   The Auth Service - Authentication via AJAX Groovy scripts are accessible to AJAX-powered HTML apps with Axeda instance credentials.  To obtain a session from an Axeda server, you should make a GET call to the Authentication service. The service is located at the following example URL: https://{{YourHostName}}/services/v1/rest/Auth/login This service accepts a valid username/password combination in the incoming Request parameters and returns a SessionID. The parameter names it expects to see are as follows: Property Name Description principal.username The username for the valid Axeda credential. password The password for the supplied credential. A sample request to the Auth Service: GET: https://{{YourHostName}}/services/v1/rest/Auth/login?principal.username=YOURUSER&password=YOURPASS Would yield this response (at this time the response is always in XML): <ns1:WSSessionInfo xsi:type="ns1:WSSessionInfo" xmlns:ns1="http://type.v1.webservices.sl.axeda.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">   <ns1:created>2013-08-12T13:19:37 +0000</ns1:created>   <ns1:expired>false</ns1:expired>   <ns1:sessionId>19c33190-dded-4655-b2c0-921528f7b873</ns1:sessionId> <ns1:sessionTimeout> 1800 </ns1:sessionTimeout> </ns1:WSSessionInfo>       The response fields are as follows: Field Name Description created The timestamp for the date the session was created expired A boolean indicating whether or not this session is expired (should be false) sessionId The ID of the session which you will use in subsequent requests sessionTimeout The time (in seconds) that this session will remain active for The Auth Service is frequently invoked from JavaScript as part of Custom Applications. The following code demonstrates this style of invocation. function authenticate(host, username, password) {             try {                 netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");             } catch (e) {                 // must be IE             }             var xmlHttpReq = false;             var self = this;             // Mozilla/Safari             if (window.XMLHttpRequest) {                 self.xmlHttpReq = new XMLHttpRequest();             }             // IE             else if (window.ActiveXObject) {                 self.xmlHttpReq = new ActiveXObject("Microsoft.XMLHTTP");             }             var SERVICES_PATH = "/services/v1/rest/"             var url = host + SERVICES_PATH + "Auth/login?principal.username=" + username + "&password=" + password;             self.xmlHttpReq.open('GET', url, true);             self.xmlHttpReq.onreadystatechange = function() {                 if (self.xmlHttpReq.readyState == 4) {                     getSessionId(self.xmlHttpReq.responseXML);                 }             }             self.xmlHttpReq.send() } function getSessionId(xml) {             var value             if (window.ActiveXObject) {                 // xml traversing with IE                 var objXML = new ActiveXObject("MSXML2.DOMDocument.6.0");                 objXML.async = false;                 var xmldoc = objXML.loadXML(xml);                 objXML.setProperty("SelectionNamespaces", "xmlns:ns1='http://type.v1.webservices.sl.axeda.com'");                 objXML.setProperty("SelectionLanguage","XPath");                 value =  objXML.selectSingleNode("//ns1:sessionId").childNodes[0].nodeValue;             } else {                 // xml traversing in non-IE browsers                 var node = xml.getElementsByTagNameNS("*", "sessionId")                 value = node[0].textContent             }             return value } authenticate ("http://mydomain.axeda.com", "myUsername", "myPassword")       Calling Scripto via AJAX Once you have obtained a session id through authentication via AJAX, you can use that session id in Scripto calls. The following is a utility function which is frequently used to wrap Scripto invocations from a UI. function callScripto(host, scriptName, sessionId, parameter) {             try {                 netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");             } catch (e) {                 // must be IE             }             var xmlHttpReq = false;             var self = this;             // Mozilla/Safari             if (window.XMLHttpRequest) {                 self.xmlHttpReq = new XMLHttpRequest();             }             // IE             else if (window.ActiveXObject) {                 self.xmlHttpReq = new ActiveXObject("Microsoft.XMLHTTP");             }             var url = host + SERVICES_PATH + "Scripto/execute/" + scriptName + "?sessionid=" + sessionId;             self.xmlHttpReq.open('GET', url, true);             self.xmlHttpReq.onreadystatechange = function() {                 if (self.xmlHttpReq.readyState == 4) {                     updatepage(div, self.xmlHttpReq.responseText);                 }             }             self.xmlHttpReq.send(parameter); } function updatepage(div, str) {             document.getElementById(div).innerHTML = str; } callScripto("http://mydomain.axeda.com", "myGroovyScriptName", "mySessionId", "myparameter=foo")       A more modern jQuery-based example might look like the following: function callScripto(host, scriptName, sessionId, parameter) {     var url = host + '/services/v1/rest/Scripto/execute/' + scriptName + '?sessionid=' + sessionId     if ( parameter != null ) url += '&' + parameter     $.ajax({url: url,               success:  function(response) {  updatepage(div, response); }           }); } function updatepage(div, str) {     $("#" + div).innerHTML = str } callScripto("http://mydomain.axeda.com", "myGroovyScriptName", "mySessionId", "myparameter=foo") In Conclusion As shown above, Scripto offers a number of ways to interact with the platform.  On each version of the Axeda Platform, all supported v1 and v2 APIs are available for Scripto to interact with the Axeda domain objects and implement business logic to solve real-world customer problems. Bibliography ​(PTC.net account required)     Axeda v2 API/Services Developer's Reference Version 6.8.3 August 2015     Axeda® v1 API Developer’s Reference Guide Version 6.8 August 2014     Documentation Map for Axeda® 6.8.2 January 2015
View full tip
Prerequisite Download the .NET SDK from the PTC Support Portal and set up the SteamSensor Example according the directions found in the ThingWorx Help Center SDK Steam Sensor Example In ThingWorx Create a Remote thing using the RemoteThingWithFileTransfer template (SteamSensor1 in example) Create a file repository and execute the CreateFolder service to create a folder in the repository folder in ThingworxStorage (MyRepository in example) In SteamThing.cs At the top of the file, import the file transfer class using com.thingworx.communications.client.things.filetransfer;” Create a virtual thing that extends FileTransferVirtualThing E.g. using steam sensor Thing public class SteamThing : FileTransferVirtualThing Edit SteamThing as follows {               public SteamThing(string name, string description, string identifier, ConnectedThingClient client, Dictionary<string, string> virtualDirectories)             : base(name, description, client, virtualDirectories) } In Client.cs Create a new Dictionary above the Steam Things. Select any name you wish as the virtual directory name and set the directory path. In this example, it is named EdgeDirectory and set to the root of the C Drive. Dictionary<string, string> virtualDirectories = new Dictionary<string, string>()             {                 {"EdgeDirectory", "C:\\"}             }; Modify the SteamThing to include your newly created virtual directories in the SteamThing parameters // Create two Virtual Things SteamThing sensor1 = new SteamThing("SteamSensor1", "1st Floor Steam Sensor", "SN0001", client, virtualDirectories); SteamThing sensor2 = new SteamThing("SteamSensor2", "2nd Floor Steam Sensor", "SN0002", client, virtualDirectories); To send or receive a file from the server, it is recommended that the built in GetFile and Send File are used. Create a remote service in the SDK containing either GetFile or SendFile GetFile — Get a file from the Server. sourceRepo — The entityName to get the file from. sourcePath — The path to the file to get. sourceFile — Name of the file to get. targetPath — The local VIRTUAL path of the resulting file (not including the file name). targetFile — Name of the resulting file in the target directory. timeout — Timeout, in seconds, for the transfer. A zero will use the systems default timeout. async — If true return immediately and call a callback function when the transfer is complete if false, block until the transfer is complete. Note that the file callback function will be called in any case. E.g. GetFile("MyRepository", "/", "test.txt", "EdgeDirectory", "movedFile.txt", 10000, true); SendFile — Sends a file to the Server. This method takes the following parameters: sourcePath — The VIRTUAL path to the file to send (not including the file name). sourceFile — Name of the file to send. targetRepo — Target repostiory of the file. targetPath — Path of the resulting file in the target repo (not including the file name). targetFile — Name of the resulting file in the target directory. timeout — Timeout, in seconds, for the transfer. A zero will use the systems default timeout. async — If true return immediately and call a callback function when the transfer is complete if false, block until the transfer is complete. Note that the file callback function will be called in any case. E.g. SendFile("/EdgeDirectory", "test.txt", "MyRepository", "/", "movedFile.txt",  10000,  true); From Composer, bring in the Remote Service on the SteamSensor thing and execute it. Files can now be transferred to or from the .NET SDK
View full tip
The .net-sdk can be configured to emit very detailed debugging and diagnostic information to a log file during execution. The .net-sdk uses the standard .NET System.Diagnostic infrastructure for Logging, as such, all configuration of the .net-sdk logger is done via the standard .NET Logging configuration system. By default, Logging is configured via the standard .NET “App.config” file. Log messages can be routed to any standard .NET TraceListener. Optionally, ThingWorx provides a FixedFieldTraceListener which can be used to output log messages to a file. The use of the ThingWorx provided FixedFieldTraceListener is recommended. The FixedFieldTraceListener when configured will automatically create a "logs" directory in the same location as (a sibling to) the running executable file (.exe). This "logs" directory will contain the log files. Every .NET Class can be configured as a specific “Trace Source” which emits log messages. It is recommended to add at least the following Trace Sources to your App.config file to receive the most useful amount of information: com.thingworx.communications.client.BaseClient com.thingworx.communications.client.ConnectedThingClient com.thingworx.communications.client.things.VirtualThing com.thingworx.communications.client.TwApiWrapper com.thingworx.communications.client.things.filetransfer.FileTransferVirtualThing com.thingworx.communications.client.things.contentloader.ContentLoaderVirtualThing The amount of information emitted can range from very low level Trace messages (the Verbose setting) to nothing at all (the Off setting). The “SourceLevels Enumeration” can be used to control how much information is written out to the log file. For reference, this is the <add name="SourceSwitch" value="Information" /> element in the sample below. Below is sample App.config file. <?xml version="1.0" encoding="utf-8"?> <configuration>     <system.diagnostics>       <sources>         <source name="com.thingworx.common.utils.JSONUtilities" switchName="SourceSwitch" switchType="System.Diagnostics.SourceSwitch" >           <listeners>             <add name="file" />           </listeners>         </source>         <source name="com.thingworx.communications.client.TwApiWrapper" switchName="SourceSwitch" switchType="System.Diagnostics.SourceSwitch" >           <listeners>             <add name="file" />           </listeners>         </source>         <source name="com.thingworx.communications.client.BaseClient" switchName="SourceSwitch" switchType="System.Diagnostics.SourceSwitch" >           <listeners>             <add name="file" />           </listeners>         </source>         <source name="com.thingworx.communications.client.ConnectedThingClient" switchName="SourceSwitch" switchType="System.Diagnostics.SourceSwitch" >           <listeners>             <add name="file" />           </listeners>         </source>         <source name="com.thingworx.communications.client.things.contentloader.ContentLoaderVirtualThing" switchName="SourceSwitch" switchType="System.Diagnostics.SourceSwitch" >           <listeners>             <add name="file" />           </listeners>         </source>         <source name="com.thingworx.communications.client.things.filetransfer.FileTransferVirtualThing" switchName="SourceSwitch" switchType="System.Diagnostics.SourceSwitch" >           <listeners>             <add name="file" />           </listeners>         </source>         <source name="com.thingworx.communications.client.things.VirtualThing" switchName="SourceSwitch" switchType="System.Diagnostics.SourceSwitch" >           <listeners>             <add name="file" />           </listeners>         </source>         <source name="com.thingworx.metadata.annotations.MetadataAnnotationParser" switchName="SourceSwitch" switchType="System.Diagnostics.SourceSwitch" >           <listeners>             <add name="file" />           </listeners>         </source>       </sources>       <switches>         <add name="SourceSwitch" value="Information" />       </switches>       <sharedListeners>         <add name="file" type="com.thingworx.common.logging.FixedFieldTraceListener, thingworx-dotnet-common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" initializeData="false"/>       </sharedListeners>       <trace autoflush="true" indentsize="4" />   </system.diagnostics> </configuration>
View full tip
Applicable Releases: ThingWorx Platform 7.0 to 8.5   Description:   Covers the main topics that need to be considered when evaluating or designing a scalability strategy for the environment and applications The agenda contains the following topics: Introduction Connection Server Federation High Availability       Some of the databases mentioned in the presentation are no longer supported Please refer to the compatibility matrix to check supported databases Related Articles How to configure a Federate architecture Is high availability supported in ThingWorx  
View full tip
Applicable Releases: ThingWorx Platform 7.0 to 8.5   Description:   Introduction to Edge connectivity in Thingworx Foundation: Edge concept and definition Available edge products Why use Edge products What is Edge Microserver and Lua Script Resource What are the SDKs What are connection servers AlwaysOn and HTTP protocols ThingTemplates to connect remote devices     The session was recorded in an old ThingWorx version, but all the concepts are still applicable
View full tip
Hiya,   I recently prepared a short demo which shows how to onboard and use Azure IoT devices in ThingWorx and added some usability tips and tricks to help others who might struggle with some of the things that I did.     The good news... I recorded and posted it to YouTube here.   •Connect Azure IoT Hub with ThingWorx (to be updated soon for 9.0 release) •Using the Azure IoT Dev Kit with ThingWorx •Getting the Azure IoT Hub Connector Up and Running (V3/8.5)   Enjoy, and don't hesitate to comment with your own tips and feedback.   Cheers,   Greg
View full tip
Hi Community,   Although we have reference architectures and integration paths for connecting devices to ThingWorx through Azure IoT; no one has ever written anything about doing the same from one ThingWorx to another.  I thought I’d change that and put some ideas out there around how one might go about doing this.  Although this is not officially supported or recommended by PTC; I have consulted with a number of leading SMEs on the subject, which have participated in forming the basis of my thinking outlined here.   Components Required (in order of communication path): On-premise ThingWorx Platform Protocol Adapter Toolkit* (CXS) - MQTT Azure IoT Edge Azure IoT Hub ThingWorx Azure IoT Hub Connector (CXS) Azure Cloud-hosted ThingWorx Platform   PAT (2) with codec to encode MQTT messages publishes to on-premise IoT Edge MQTT endpoint which handles store-and-forward of messages to IoT Hub.  An Azure IoT device would exist for each Thing you wish to represent on the ThingWorx servers.  The Azure IoT Hub Connector would pick-up the incoming messages and pass them on to the cloud ThingWorx which would decode the MQTT payload and map to Thing property updates.   The only part that I presently don’t like about this approach is that you’ll need to decode the MQTT messages on the ThingWorx platform in the cloud when they are received from the IoT Hub, and this mechanism will need to also need to handle encoding and publishing back to the IoT Hub if C2D (Cloud-to-Device) messages are to be implemented (aka bi-directional).  This is required as ThingWorx only supports AlwaysOn as an application level protocol so some form of mapping needs to be done.   * Another approach would be to replace the PAT with a custom agent which implements both the ThingWorx Edge SDK and the Azure IoT device SDK   Regards,   Greg Eva
View full tip
Developing with Axeda Artisan (for Axeda Platform, v6.8 and later) Axeda Artisan is a development tool based on the Apache Maven build system. Artisan includes authoring, management, and deployment mechanisms that define and manage the development of Axeda Platform extension points, allowing you to flexibly install, update, and uninstall many types of objects on the Axeda Platform. The Components of Artisan The Artisan framework is comprised of several components: The Axeda Platform Each instance of the Axeda Platform contains libraries and sample Artisan Projects (archetypes) that are downloaded by developers to author and deploy their own Artisan Projects. The Artisan Project An Artisan Project is a collection of content and configuration files that are used to define a set of objects that will be installed on the Axeda Platform. The Artisan Installer The Artisan Installer is a flexible, configurable application that uses the Apache Maven build life-cycle to upload the contents of an Artisan Project to the Axeda Platform. Axeda Platform APIs The Artisan Installer interacts with the Axeda Platform and uses Axeda APIs to install, upgrade, or uninstall ArtisanProjects. How You Work With Artisan The goal of using Artisan is to develop an Artisan Project that will become an object or application that runs on the Axeda Platform. There are several phases involved in creating an Artisan Project: Preparing Your Development Environment – When first working with Artisan, you need to prepare your development environment by installing or configuring Java, Apache Maven, and any other required development tools. Creating an Artisan Project – Artisan uses archetypes as the basis for Artisan Projects. Archetypes are used to generate an Artisan Project that contains the correct type of development framework, which you then modify to create your own Artisan Project: The Hello World archetype provides a sample project you can use to create common objects on the Axeda Platform. The Machine Streams archetype provides a sample project you can use to configure machine streams for your Axeda Platform. Installing an Artisan Project on the Axeda Platform – The Artisan Installer packages the contents of the Artisan Project, and deploys them on the Axeda Platform. Testing the Artisan Project – Once installed, you can test your Artisan Project to verify its functionality and identify any issues that need to be addressed. It is common for an Artisan Project to be installed on the Axeda Platform several times as issues are identified and corrected during development. Creating a Stand-alone Installer – Once ready for deployment to production, the Artisan Installer is used to create a stand-alone installer for distributing the project contents to other instances of the Axeda Platform. Where to Go from Here To begin working with the Artisan framework to create your own projects, refer to the following: Artisan Landing Page – A page hosted on the Axeda Platform that introduces Artisan and serves as a starting point for creating an Artisan Project. The Artisan Landing Page can be found at the following location: http://instance_name:port/artisan                     Where instance_name:port/ is the ip address and port of an instance of the Axeda Platform. Axeda® Artisan Developer’s Guide – A reference that introduces Artisan, describes how to prepare your development environment, and provides detailed technical information concerning creating an Artisan Project and configuring the Artisan Installer. The Axeda® Artisan Developer’s Guide is available with all Axeda product documentation from the PTC Support site, http://support.ptc.com/
View full tip
Complete information about installing and using the Axeda Machine Streams Data Relay Project is available here. This page provides the files for Axeda Machine Streams Data Relay Project: For Linux users ready to run (bin) files: download the machine-streams-data-relay-1.0.3-bin.tar_.gz archive full Maven project files: download the machine-streams-data-relay-1.0.3-project.tar_.gz archive For Windows users ready to run (bin) files: download the machine-streams-data-relay-1.0.3-bin.zip archive full Maven project files: download the machine-streams-data-relay-1.0.3-project.zip archive From the links below, select the project you want to use.
View full tip
This Zip file contains the Axeda patch (axeda-jms-plugin-<version>-machine-streams) required for proper installation and configuration of an Apache ActiveMQ server to use with the Axeda Machine Streams service (which is supported for Axeda Platform v6.8 and later). Note: Information about the Axeda Machine Streams feature is provided in the Axeda Features Guide available from the Axeda Support site, http://help.axeda.com. This patch overlay needs to be applied to the v5.8.0 ActiveMQ server installed as the Axeda Machine Streams endpoint broker, so that Axeda Platform can send streamed content to that server endpoint. Complete instructions for installing and configuring an Apache ActiveMQ server for Axeda Machine Streams are provided in the reference, Axeda® Machine Streams: A Guide to Setting Up Broker Endpoints. This guide is available with all Axeda product documentation from the PTC Support site.
View full tip
A student in the Axeda Groovy course had some good questions. Instead of answering via email, I thought I'd answer here, so we could share the knowledge. Domain Objects - How can I customize or extend Axeda provided domain objects? (Eg., I need to store whether a user is external or internal user) How can I achieve this? This is a perfect use case for the Axeda Extended Data API. Documentation on the API can be found in the Axeda Platform v1 API Developer's Reference Guide. A good, "Getting Started," topic is available on Axeda Mentor - Getting Started With the Axeda Extended Data API. To customize/extend/decorate Axeda-provided domain objects, use Extended Objects. Extended Objects can be thought of in the abstract as a collection of custom database table rows. The analogy isn't perfect, but it helps with understanding. The use case in the question is very common. Extended Objects can stand on their own, or they can be associated with an Axeda domain object with the setInternalID() method. In the following sample code, we "decorate" an Axeda User object with two new fields, isExternalUser and companyID: private void updateAdditionalFieldsForUser(User user, boolean isExternalUser, String companyID) {   // GET OBJECT TYPE - This example assumes the ExtendedObjectType has already been created   ExtendedObjectType extObjectType = extendedObjectService.findExtendedObjectTypeByClassname("com.axeda.drm.sdk.user.User")   // GET PROPERTY TYPES   PropertyType isExternalUserPropertyType = findOrCreatePropertyType(extObjectType, "IS_EXTERNAL_USER")   PropertyType companyIdPropertyType = findOrCreatePropertyType(extObjectType, "COMPANY_ID")      /* GET THE EXTENDED OBJECT   * Note - the example provides a findExtendedObject() method that searches by INTERNAL ID. This linkage via the internal ID   * associates a domain object to an ExtendedObject, allowing us to "decorate" it with custom attributes   */   ExtendedObject extObject = findExtendedObject(extObjectType, user.id.value)   if (extObject == null)   {     extObject = new ExtendedObject()     extObject.setExtendedObjectType(extObjectType)     extObject.setInternalObjectId(user.id.value)     extObject.addProperty(createExtendedProperty(isExternalUserPropertyType, isExternalUser.toString()))     extObject.addProperty(createExtendedProperty(companyIdPropertyType, companyID))     extendedObjectService.createExtendedObject(extObject)   }   else   {     addOrUpdateExtendedProperty(extObject, isExternalUserPropertyType, "IS_EXTERNAL_USER", isExternalUser.toString())     addOrUpdateExtendedProperty(extObject, companyIdPropertyType, "COMPANY_ID", companyID)     extendedObjectService.updateExtendedObject(extObject)   } }  /**  * Adds or Updates an extended property.  *  * @param extendedObject the extended object to associate with the property  * @param propertyType the type of the property  * @param propertyName the name of the property  * @param propertyValue the value of the property  */ private void addOrUpdateExtendedProperty(ExtendedObject extendedObject, PropertyType propertyType, String propertyName, String propertyValue) {   Property extendedProperty = extendedObject.getPropertyByName(propertyName)   if (extendedProperty == null)   {   extendedProperty = createExtendedProperty(propertyType, propertyValue)   extendedObject.addProperty(extendedProperty)   }   else   {   extendedProperty.setValue(propertyValue)   } }  /**  * Retrieves and returns the extended object with the given type and internal id.  *  * @param extObjectType the type of the desired external object  * @param internalObjectId the internal id of the desired extended object  * @return the extended object matching the given details or <code>null</code> in case there's no match  */ private ExtendedObject findExtendedObject(ExtendedObjectType extObjectType, Long internalObjectId) {   ExtendedObjectSearchCriteria searchCriteria = new ExtendedObjectSearchCriteria()   searchCriteria.setExtendedObjectTypeId(extObjectType.getId())   searchCriteria.setInternalObjectId(internalObjectId)    List<ExtendedObject> extObjects = extendedObjectService.findExtendedObjects(searchCriteria, -1, 0, "name")    if (!extObjects?.isEmpty())   {    return extObjects[0]   }   return null }  /**  * Gets a property type given its name and the associated extended object type.  * If it cannot find the property type it will create it.  *  * @param extendedObjectType the associated extended object type for the property type  * @param propertyTypeName the property type name  * @return the property type  */  private PropertyType findOrCreatePropertyType(ExtendedObjectType extendedObjectType, String propertyTypeName)  {     PropertyType propertyType = extendedObjectType.getPropertyTypeByName(propertyTypeName)     if (propertyType == null)     {       propertyType = new PropertyType()       propertyType.setDataType(PropertyDataType.String)       propertyType.setName(propertyTypeName)       propertyType.setExtendedObjectType(extendedObjectType)       extendedObjectService.createPropertyType(propertyType)       extendedObjectType.addPropertyType(propertyType)     }     return propertyType  } By using Extended Objects, and associating them with Axeda domain objects via an internal ID, we can decorate/extend/customize those domain objects with use-case-specific attributes.
View full tip