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

ThingWorx Navigate is now Windchill Navigate Learn More

IoT & Connectivity Tips

Sort by:
This tutorial applies to Axeda version 6.1.6+, with sections applicable to 6.5+ (indicated below) Custom objects (or Groovy scripts) are the backbone of Axeda custom applications.  As the developer, you decide what content type to give the data returned by the script. What this tutorial covers? This tutorial provides examples of outputting data in different formats from Groovy scripts and consuming that data via Javascript using the jQuery framework.  While Javascript and jQuery are preferred by the Axeda Innovation team, any front end technology that can consume web services can be used to build applications on the Axeda Machine Cloud.  On the same note, the formats discussed in this article are only a few examples of the wide variety of content types that Groovy scripts can output via Scripto.  The content types available via Scripto are limited only by their portability over the TCP protocol, a qualification which includes all text-based and downloadable binary mime types.  As of July 2013, the UDP protocol (content streaming) is not supported by the current version of the Axeda Platform. Formats discussed in this article: 1) JSON 2) XML 3) CSV 4) Binary content with an emphasis on image files (6.5+) For a tutorial on how to create custom objects that work with custom applications, check out Using Google Charts API with Scripto.  For a discussion of what Scripto is and how it relates to Groovy scripts and Axeda web services, take a look at Unleashing the Power of the Axeda Platform via Scripto. Serializing Data JSON For those building custom applications with Javascript, serializing data from scripts into JSON is a great choice, as the data is easily consumable as native Javascript objects. The net.sf.json JSON library is available to use in the SDK.  It offers an easy way to serialize objects on the Platform, particularly v2 SDK objects. import net.sf.json.JSONArray import static com.axeda.sdk.v2.dsl.Bridges.* def asset = assetBridge.findById(parameters.assetId) def response = JSONArray.fromObject(asset).toString(2) return ["Content-Type": "application/json", "Content": response] Outputs: [{     "buildVersion": "",     "condition": {         "detail": "",         "id": "3",         "label": "",         "restUrl": "",         "systemId": "3"     },     "customer": {         "detail": "",         "id": "2",         "label": "Default Organization",         "restUrl": "",         "systemId": "2"     },     "dateRegistered": {         "date": 11,         "day": 1,         "hours": 18,         "minutes": 7,         "month": 2,         "seconds": 49,         "time": 1363025269253,         "timezoneOffset": 0,         "year": 113     },     "description": "",     "detail": "testasset",     "details": null,     "gateways": [],     "id": "12345",     "label": "",     "location": {         "detail": "Default Organization",         "id": "2",         "label": "Default Location",         "restUrl": "",         "systemId": "2"     },     "model": {         "detail": "testmodel",         "id": "2345",         "label": "standalone",         "restUrl": "",         "systemId": "2345"     },     "name": "testasset",     "pingRate": 0,     "properties": [         {             "detail": "",             "id": "1",             "label": "TestProperty",             "name": "TestProperty",             "parentId": "2345",             "restUrl": "",             "systemId": "1",             "value": ""         },         {             "detail": "",             "id": "4",             "label": "TestProperty0",             "name": "TestProperty0",             "parentId": "2345",             "restUrl": "",             "systemId": "4",             "value": ""         },         {             "detail": "",             "id": "3",             "label": "TestProperty1",             "name": "TestProperty1",             "parentId": "2345",             "restUrl": "",             "systemId": "3",             "value": ""         },         {             "detail": "",             "id": "2",             "label": "TestProperty2",             "name": "TestProperty2",             "parentId": "2345",             "restUrl": "",             "systemId": "2",             "value": ""         }     ],     "restUrl": "",     "serialNumber": "testasset",     "sharedKey": [],     "systemId": "12345",     "timeZone": "GMT" }] This output can be traversed as Javascript object with its nodes accessible using dot (.) notation. For example, if you set the above JSON as the content of variable "json", you can access it in the following way, without any preliminary parsing needed: assert json[0].condition.id == 3 If you use jQuery, a Javascript library, feel free to make use of axeda.js, which contains utility functions to pass data to and from the Axeda Platform.  One function in particular is used in most example custom applications found on this site, the axeda.callScripto function.  It relies on the jQuery ajax function to make the underlying call. /**   * makes a call to the enterprise platform services with the name of a script and passes   * the script any parameters provided.   *   * default is GET if the method is unknown   *   * Notes: Added POST semantics - plombardi @ 2011-09-07   *   * original author: Zack Klink & Philip Lombardi   * added on: 2011/7/23   */ // options - localstoreoff: "yes" for no local storage, contentType: "application/json; charset=utf-8", axeda.callScripto = function (method, scriptName, scriptParams, attempts, callback, options) {   var reqUrl = axeda.host + SERVICES_PATH + 'Scripto/execute/' + scriptName + '?sessionid=' + SESSION_ID   var contentType = options.contentType ? options.contentType : "application/json; charset=utf-8"   var local   var daystring = keygen()   if (options.localstoreoff == null) {   if (localStorage) {   local = localStorage.getItem(scriptName + JSON.stringify(scriptParams))   }   if (local != null && local == daystring) {   return dfdgen(reqUrl + JSON.stringify(scriptParams))   } else {   localStorage.setItem(scriptName + JSON.stringify(scriptParams), daystring)   }   }   return $.ajax({   type: method,   url: reqUrl,   data: scriptParams,   contentType: contentType,   dataType: "text",   error: function () {   if (attempts) {   expiredSessionLogin();   setTimeout(function () {   axeda.callScripto('POST', scriptName, scriptParams, attempts - 1, callback, options)   }, 1500);   }   },   success: function (data) {   if (options.localstoreoff == null) {   localStorage.setItem(reqUrl + JSON.stringify(scriptParams), JSON.stringify([data]))   }   if (contentType.match("json")) {   callback(unwrapResponse(data))   } else {   callback(data)   }   }   }) }; Using the axeda.callScripto function: var postToPlatform = function (scriptname, callback, map) {         var options = {             localstoreoff: "yes",             contentType: "application/json; charset=utf-8"         }        // Javascript object "map" has to be stringified to post to Axeda Platform         axeda.callScripto("POST", scriptname, JSON.stringify(map), 2, function (json) {             // callback gets the JSON object output by the Groovy script             callback(json)         }, options)     } The JSON object is discussed in more detail here. Back to Top XML XML is the preferred language of integration with external applications and services. Groovy provides utilities to make XML serialization a trivial exercise. import groovy.xml.MarkupBuilder import static com.axeda.sdk.v2.dsl.Bridges.* def writer = new StringWriter() def xml = new MarkupBuilder(writer) def findAssetResult = assetBridge.find(new AssetCriteria(modelNumber: parameters.modelName)) // find operation returns AssetReference class. Contains asset id only def assets = findAssetResult.assets      xml.Response() {   Assets() {   assets.each { AssetReference assetRef ->   def asset = assetBridge.findById(assetRef.id)               // asset contains a ModelReference object instead of a Model.  ModelReference has a detail property, not a name property   Asset() {   id(asset.id)   name(asset.name)   serial_number(asset.serialNumber)   model_id(asset.model.id)   model_name(asset.model.detail)   }   }   }   } return ['Content-Type': 'text/xml', 'Content': writer.toString()] Output: <Assets>   <Asset>   <id>98765</id>   <name>testasset</name>   <serial_number>testasset</serial_number>   <model_id>4321</model_id>   <model_name>testmodel</model_name>   </Asset> </Assets Although XML is not a native Javascript object as is JSON, Javascript libraries and utilities are available for parsing XML into an object traversable in Javascript. For more information on parsing XML in Javascript, see W3 Schools XML Parser.  For those using jQuery, check out the jQuery.parseXML function. Back to Top Outputting Files (Binary content types) CSV CSV comes in handy for spreadsheet generation as it is compatible with Microsoft Excel. The following example is suitable for Axeda version 6.1.6+ as it makes use of the Data Accumulator feature to create a downloadable file. import com.axeda.drm.sdk.device.ModelFinder import com.axeda.drm.sdk.Context import com.axeda.drm.sdk.scripto.Request import com.axeda.common.sdk.id.Identifier import com.axeda.drm.sdk.device.Model import com.axeda.drm.sdk.device.DataItem import com.axeda.drm.sdk.device.DataItemValue import com.axeda.drm.sdk.data.DataValue import com.axeda.drm.sdk.device.DeviceFinder import com.axeda.drm.sdk.device.Device import com.axeda.drm.sdk.mobilelocation.MobileLocation import com.axeda.drm.sdk.data.DataValueList import com.axeda.drm.sdk.data.CurrentDataFinder import com.axeda.drm.sdk.mobilelocation.CurrentMobileLocationFinder import groovy.xml.MarkupBuilder import com.axeda.platform.sdk.v1.services.ServiceFactory /* * ExportObjectToCSV.groovy * * Creates a csv file from either all assets of a model of a single asset that can then be used to import them back into another system. * * @param model        -   (REQ):Str model name. * @param serial        -   (OPT):Str serial number. * * @author Sara Streeter <sstreeter@axeda.com> */ def writer = new StringWriter() def xml = new MarkupBuilder(writer) InputStream is try {    Context CONTEXT = Context.getSDKContext()    ModelFinder modelFinder = new ModelFinder(CONTEXT)     modelFinder.setName(Request.parameters.model)    Model model = modelFinder.find()    DeviceFinder deviceFinder = new DeviceFinder(CONTEXT)    deviceFinder.setModel(model)    List<Device> devices = [] def exportkey = model.name Device founddevice if (Request.parameters.serial){     deviceFinder.setSerialNumber(Request.parameters.serial)    founddevice = deviceFinder.find()    logger.info(founddevice?.serialNumber)    if (founddevice != null){    devices.add(founddevice)    }    else throw new Exception("Device ${Request.parameters.serial} cannot be found.")    exportkey += "${founddevice.serialNumber}" } else {     devices = deviceFinder.findAll()     exportkey += "all" } // use a Data Accumulator to store the information def dataStoreIdentifier = "FILE-CSV-export_____" + exportkey def daSvc = new ServiceFactory().dataAccumulatorService if (daSvc.doesAccumulationExist(dataStoreIdentifier, devices[0].id.value)) {   daSvc.deleteAccumulation(dataStoreIdentifier, devices[0].id.value) } List<DataItem> dataItemList = devices[0].model.dataItems def firstrow = [ "model", "serial", "devicename", "conditionname", "currentlat","currentlng" ]                     def tempfirstrow = dataItemList.inject([]){list, dataItem ->             list << dataItem.name;             list         }         firstrow += tempfirstrow            firstrow = firstrow.join(',')         firstrow += '\n'         daSvc.writeChunk(dataStoreIdentifier, devices[0].id.value, firstrow);     CurrentMobileLocationFinder currentMobileLocationFinder = new CurrentMobileLocationFinder(CONTEXT) devices.each{ device ->                 CurrentDataFinder currentDataFinder = new CurrentDataFinder(CONTEXT, device)                 currentMobileLocationFinder.deviceId = device.id.value                 MobileLocation mobileLocation = currentMobileLocationFinder.find()                 def lat = 0                 def lng = 0                 if (mobileLocation){                     lat = mobileLocation?.lat                     lng = mobileLocation?.lng                 }                 def row =                 [                     device.model.name,                     device.serialNumber,                     device.name,                     device.condition?.name,                     lat,                     lng                     ]                                     def temprow = dataItemList.inject([]){ subList,dataItem ->                         DataValue value = currentDataFinder.find(dataItem.name)                                             def val = "NULL"                         val = value?.asString() != "?" ? value?.asString() : val                         subList <<  val                         subList                     }                 row += temprow                 row = row.join(',')                 row += '\n'                 daSvc.writeChunk(dataStoreIdentifier, devices[0].id.value, row);             }    // stream the data accumulator to create the file is = daSvc.streamAccumulation(dataStoreIdentifier, devices[0].id.value) def disposition = 'attachment; filename=CSVFile' + exportkey + '.csv' return ['Content-Type': 'text/csv', 'Content-Disposition':disposition, 'Content': is.text] } catch (def ex) {    xml.Response() {        Fault {            Code('Groovy Exception')            Message(ex.getMessage())            StringWriter sw = new StringWriter();            PrintWriter pw = new PrintWriter(sw);            ex.printStackTrace(pw);            Detail(sw.toString())        }    } logger.info(writer.toString()) return ['Content-Type': 'text/xml', 'Content': writer.toString()] } return ['Content-Type': 'text/xml', 'Content': writer.toString()] Back to Top Image Files (6.5+) The FileStore in Axeda version 6.5+ allows fine-grained control of uploaded and downloaded files. As Groovy scripts can return binary data via Scripto, this allows use cases such as embedding a Groovy script url as the source for an image. The following example uses the FileStore API to create an Image out of a valid image file, scales it to a smaller size and stores this smaller file. import com.axeda.drm.sdk.Context import com.axeda.drm.sdk.data.* import com.axeda.drm.sdk.device.* import com.axeda.drm.sdk.mobilelocation.CurrentMobileLocationFinder import com.axeda.drm.sdk.mobilelocation.MobileLocation import com.axeda.drm.sdk.mobilelocation.MobileLocationFinder import com.axeda.sdk.v2.bridge.FileInfoBridge import static com.axeda.sdk.v2.dsl.Bridges.* import com.axeda.services.v2.ExecutionResult import com.axeda.services.v2.FileInfo import com.axeda.services.v2.FileInfoReference import com.axeda.services.v2.FileUploadSession import net.sf.json.JSONObject import groovy.json.JsonBuilder import net.sf.json.JSONArray import com.axeda.drm.sdk.scripto.Request import org.apache.commons.io.IOUtils import org.apache.commons.lang.exception.ExceptionUtils import com.axeda.common.sdk.id.Identifier import groovy.json.* import javax.imageio.ImageIO; import java.awt.RenderingHints import java.awt.image.BufferedImage import java.io.ByteArrayOutputStream; import java.awt.* import java.awt.geom.* import javax.imageio.* import java.awt.image.* import java.awt.Graphics2D import javax.imageio.stream.ImageInputStream /*    Image-specific FileStore entry point to post and store files */ def contentType = "application/json" final def serviceName = "StoreScaledImage" // Create a JSON Builder def json = new JsonBuilder() // Global try/catch. Gotta have it, you never know when your code will be exceptional! try {       Context CONTEXT = Context.getSDKContext()     def filesList = []     def datestring = new Date().time     InputStream inputStream = Request.inputStream       def reqbody = Request.body     // all of our Request Parameters are available here     def params = Request.parameters     def filename = Request?.headers?.'Content-Disposition' ?     Request?.headers?.'Content-Disposition' : "file___" + datestring + ".txt"     def filelabel = Request.parameters.filelabel ?: filename     def description = Request.parameters.description ?: filename     def contType = Request.headers?."content-type" ?: "image/jpeg"     def tag = Request.parameters.tag ?: "cappimg"     def encoded = Request.parameters.encoded?.toBoolean()   def dimlimit = params.dimlimit ? params.dimlimit : 280     // host is available in the headers when the script is called with AJAX     def domain = Request.headers?.host     byte[] bytes = IOUtils.toByteArray(inputStream);     def fileext = filename.substring(filename.indexOf(".") + 1,filename.size())     def outerMap = [:]     // check that file extension matches an image type     if (fileext ==~ /([^\s]+(\.(?i)(jpg|jpeg|png|gif|bmp))$)/){         if (inputStream.available() > 0) {                 def scaledImg                               try {                     def img = ImageIO.read(inputStream)                     def width = img?.width                              def height = img?.height                     def ratio = 1.0                     def newBytes                                       if (img){                                               if (width > dimlimit || height > dimlimit){                             // shrink by the smaller side so it can still be over the limit                             def dimtochange = width > height ? height : width                             ratio = dimlimit / dimtochange                                                       width = Math.floor(width * ratio).toInteger()                             height = Math.floor(height * ratio).toInteger()                         }                                             newBytes = doScale(img, width, height, ratio, fileext)                      if (newBytes?.size() > 0){                         bytes = newBytes                      }                     }                 }                 catch(Exception e){                     logger.info(e.localizedMessage)                                   }                                           outerMap.byteCount = bytes.size()                    FileInfoBridge fib = fileInfoBridge                 FileInfo myImageFile = new FileInfo(filelabel: filelabel,                                                     filename: filename,                                                     filesize: bytes?.size(),                                                     description: description,                                                     tags: tag                                                     )                    myImageFile.contentType = contType                    FileUploadSession fus = new FileUploadSession();                 fus.files = [myImageFile]                    ExecutionResult fer = fileUploadSessionBridge.create(fus);                 myImageFile.sessionId = fer.succeeded.getAt(0)?.id                               ExecutionResult fileInfoResult = fib.create(myImageFile)                               if (fileInfoResult.successful) {                     outerMap.fileInfoSave = "File Info Saved"                     outerMap.sessionId = "File Upload SessionID: "+fer.succeeded.getAt(0)?.id                     outerMap.fileInfoId = "FileInfo ID: "+fileInfoResult?.succeeded.getAt(0)?.id                     ExecutionResult er = fib.saveOrUpdate(fileInfoResult.succeeded.getAt(0).id,new ByteArrayInputStream(bytes))                     def fileInfoId = fileInfoResult?.succeeded.getAt(0)?.id                     String url = "${domain}/services/v1/rest/Scripto/execute/DownloadFile?fileId=${fileInfoId}"                     if (er.successful) {                         outerMap.url = url                     } else {                         outerMap.save = "false"                         logger.info(logFailure(er,outerMap))                     }                 } else {                     logger.info(logFailure(fileInfoResult, outerMap))                 }                } else {                 outerMap.bytesAvail = "No bytes found to upload"             }         } else {             outerMap.imagetype = "Extension $fileext is not a supported image file type."         }     filesList << outerMap     // return the JSONBuilder contents     // we specify the content type, and any object as the return (even an outputstream!)     return ["Content-Type": contentType,"Content":JSONArray.fromObject(filesList).toString(2)]     // alternately you may just want to serial an Object as JSON:     // return ["Content-Type": contentType,"Content":JSONArray.fromObject(invertedMessages).toString(2)] } catch (Exception e) {     // I knew you were exceptional!     // we'll capture the output of the stack trace and return it in JSON     json.Exception(             description: "Execution Failed!!! An Exception was caught...",             stack: ExceptionUtils.getFullStackTrace(e)     )     // return the output     return ["Content-Type": contentType, "Content": json.toPrettyString()] } def doScale(image, width, height, ratio, fileext){     if (image){     ByteArrayOutputStream baos = new ByteArrayOutputStream();     def bytes      def scaledImg = new BufferedImage( width, height, BufferedImage.TYPE_INT_RGB )        Graphics2D g = scaledImg.createGraphics();         g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);         g.scale(ratio,ratio)         g.drawImage(image, null, null);         g.dispose();              ImageIO.write( scaledImg, fileext, baos )       baos.flush()       bytes = baos.toByteArray()       baos.close()     }     else {         logger.info("image to be scaled is null")         return false     }   return bytes   } private void logFailure(ExecutionResult fileInfoResult, LinkedHashMap outerMap) {     outerMap.message = fileInfoResult.failures.getAt(0)?.message     outerMap.source = fileInfoResult.failures.getAt(0)?.sourceOfFailure     outerMap.details = fileInfoResult.failures.getAt(0)?.details?.toString()     outerMap.fileInfoSave = "false" } The next example makes use of the jQuery framework to upload an image to this script via an http POST. Note: This snippet is available as a jsFiddle at http://jsfiddle.net/LrxWF/18/ With HTML5 button: <input type="file" id="fileinput" value="Upload" /> var PLATFORM_HOST = document.URL.split('/apps/')[0]; // this is how you would retrieve the host on an Axeda instance var SESSION_ID = null // usually retrieved from login function included below /*** * Depends on jQuery 1.7+ and HTML5, assumes an HTML5 element such as the following: * <input type="file" id="fileinput" value="Upload" /> * **/ $("#fileinput").off("click.filein").on("click.filein", function () {     fileUpload() }) var fileUpload = function () {     $("#fileinput").off('change.fileinput')     $("#fileinput").on('change.fileinput', function (event) {         if (this.files && this.files.length > 0) {             handleFiles("http://" + PLATFORM_HOST, this.files)         }     }) } var handleFiles = function (host, files) {     $.each(files, function (index, file) {         var formData = new FormData();         var filename = file.name         formData.append(filename, file)         var url = host + '/services/v1/rest/Scripto/execute/StoreScaledImage?filelabel=' + filename + "&tag=myimg"         url = setSessionId(url)         jQuery.ajax(url, {             beforeSend: function (xhr) {                 xhr.setRequestHeader('Content-Disposition', filename);             },             cache: false,             cache: false,             processData: false,             type: 'POST',             contentType: false,             data: formData,             success: function (json) {                 refreshPage(json)                 console.log(json)             }         });     }) } var setSessionId = function (url) {     // you would already have this from logging in     return url + "&sessionid=" + SESSION_ID } var refreshPage = function (json) {     // here you would refresh your page with the returned JSON     return } /*** *  The following functions are not used in this demonstration, however they are necessary for a complete app and are found in axeda.js  http://gist.github.com/axeda/4340789 ***/     function login(username, password, success, failure) {         var reqUrl = host + SERVICES_PATH + 'Auth/login';         localStorage.clear()         return $.get(reqUrl, {             'principal.username': username,                 'password': password         }, function (xml) {             var sessionId = $(xml).find("ns1\\:sessionId, sessionId").text()             // var sessionId = $(xml).find("[nodeName='ns1:sessionId']").text(); - no longer works past jquery 1.7             if (sessionId) {                 // set the username and password vars for future logins.                         storeSession(sessionId);                 success(SESSION_ID); // return the freshly stored contents of SESSION_ID             } else {                 failure($(xml).find("faultstring").text());             }         }).error(function () {             $('#loginerror').html('Login Failed, please try again')         });     }; function storeSession(sessionId) {     var date = new Date();     date.setTime(date.getTime() + SESSION_EXPIRATION);     SESSION_ID = sessionId     document.cookie = APP_NAME + '_sessionId=' + SESSION_ID + '; expires=' + date.toGMTString() + '; path=/';     return true; }; The return JSON includes a URL that you can use as the source for images: [{   "byteCount": 14863,   "fileInfoSave": "File Info Saved",   "sessionId": "File Upload SessionID: 01234",   "fileInfoId": "FileInfo ID: 12345",   "url": "http://yourdomain.axeda.com/services/v1/rest/Scripto/execute/DownloadFile?fileId=12345" }] The DownloadFile Custom Object looks like the following: import static com.axeda.sdk.v2.dsl.Bridges.* import javax.activation.MimetypesFileTypeMap import com.axeda.services.v2.* import com.axeda.sdk.v2.exception.* import com.axeda.drm.sdk.scripto.Request def knowntypes = [          [png: 'image/png']         ,[gif: 'image/gif']         ,[jpg: 'image/jpeg']     ] def params = Request.parameters.size() > 0 ? Request.parameters : parameters def response = fileInfoBridge.getFileData(params.fileId) def fileinfo = fileInfoBridge.findById(params.fileId) def type = fileinfo.filename.substring(fileinfo.filename.indexOf('.') + 1,fileinfo.filename.size()) type = returnType(knowntypes, type) def contentType = params.type ?: (type ?: 'image/jpg') return ['Content': response, 'Content-Disposition': contentType, 'Content-Type':contentType] def returnType(knowntypes, ext){     return knowntypes.find{ it.containsKey(ext) }?."$ext" } Make sure to append a valid session id to the end of the URL when using it as the source for an image. The techniques discussed above can be applied to any type of binary file output with consideration for the type of file being processed. A Word on Streaming Content streaming such as streaming of video or audio files over UDP is not currently supported by the Axeda Platform.
View full tip
This code snippet finds an uploaded file associated with an asset and emails it to a destination email address.  It uses a data accumulator to create a temporary file. import org.apache.commons.codec.binary.Base64; import java.util.Date; import java.util.Properties; import java.io.StringWriter import java.io.PrintWriter import com.axeda.drm.sdk.Context import com.axeda.drm.sdk.data.* import com.axeda.drm.sdk.device.* import groovy.json.JsonSlurper import javax.activation.DataHandler; import javax.activation.FileDataSource; import org.apache.axiom.attachments.ByteArrayDataSource; import com.axeda.platform.sdk.v1.services.ServiceFactory; import com.thoughtworks.xstream.XStream; import javax.mail.Authenticator; import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.Multipart; import javax.mail.PasswordAuthentication; import javax.mail.Session; import javax.mail.Transport; import javax.mail.internet.AddressException; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeBodyPart; import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMultipart; try {     Context ctx = Context.create(parameters.username)     DeviceFinder dfinder = new DeviceFinder(ctx)     def bytes     dfinder.setSerialNumber(parameters.serial_number)     Device d = dfinder.find()     UploadedFileFinder uff = new UploadedFileFinder(ctx)     uff.device = d     def ufiles = uff.findAll()     UploadedFile ufile     if (ufiles.size() > 0) {         ufile = ufiles[0]         File f = ufile.extractFile()         def slurper = new JsonSlurper()         def objects = slurper.parseText(f.getText())         def bugreport = objects.objects[0].mobj_update[0].bugreport         String from = "demo@axeda.com";         String to = "destination@axeda.com";         String subject = "My file";         String mailContent = "Attaching test";         String filename = "payload.tar.gz";         def dataStoreIdentifier = "FILE-IO-SUB-testing"         def daSvc = new ServiceFactory().dataAccumulatorService         if (daSvc.doesAccumulationExist(dataStoreIdentifier, d.id.value)) {             daSvc.deleteAccumulation(dataStoreIdentifier, d.id.value)         }         daSvc.writeChunk(dataStoreIdentifier, d.id.value, bugreport);         InputStream is = daSvc.streamAccumulation(dataStoreIdentifier, d.id.value)         Base64 base64 = new Base64()         ByteArrayDataSource rawData = new ByteArrayDataSource(base64.decodeBase64(is.getBytes()));         // You need to create a properties object to store mail server         // smtp information such as the host name and the port number.         // With this properties we create a Session object from         // which we'll create the Message object.         Properties properties = new Properties();         properties.put("mail.smtp.host","mail01.bo2.axeda.com");         properties.put("mail.smtp.port", "25");         properties.put("mail.smtp.auth", "true");         Authenticator authenticator = new CustomAuthenticator();         Session session = Session.getInstance(properties, authenticator);         MimeMessage message = new MimeMessage(session);         message.setFrom(new InternetAddress(from));         message.setRecipient(Message.RecipientType.TO, new InternetAddress(to));         message.setSubject(subject);         message.setSentDate(new Date());         // Set the email message text.         MimeBodyPart messagePart = new MimeBodyPart();         messagePart.setText(mailContent);         // Set the email attachment file         MimeBodyPart attachmentPart = new MimeBodyPart();         //      FileDataSource fileDataSource = new FileDataSource(file)         attachmentPart.setDataHandler(new DataHandler(rawData))  //fileDataSource));         attachmentPart.setFileName(filename);         Multipart multipart = new MimeMultipart();         multipart.addBodyPart(messagePart);         multipart.addBodyPart(attachmentPart);         // Set the content         message.setContent(multipart);         // Send the message with attachment         Transport.send(message);     } } catch (Exception e) {     logger.info(e.message)     StringWriter logStringWriter = new StringWriter();     PrintWriter logPrintWriter = new PrintWriter(logStringWriter)     e.printStackTrace(logPrintWriter)     logger.info(logStringWriter.toString()) } // This class is the implementation of the Authenticator // Where you need to implement the getPasswordAuthentication // to provide the username and password public class CustomAuthenticator extends Authenticator {     protected PasswordAuthentication getPasswordAuthentication() {         String username = "";         String password = "";         return new PasswordAuthentication(username, password);     } } static byte[] getBytes(File file) throws IOException {     return getBytes(new FileInputStream(file)); } static byte[] getBytes(InputStream is) throws IOException {     ByteArrayOutputStream answer = new ByteArrayOutputStream(); // reading the content of the file within a byte buffer     byte[] byteBuffer = new byte[8192];     int nbByteRead /* = 0*/;     try {         while ((nbByteRead = is.read(byteBuffer)) != -1) { // appends buffer             answer.write(byteBuffer, 0, nbByteRead);         }     } finally {         is.close()     }     return answer.toByteArray(); }
View full tip
This code snippet creates then deletes a data item to illustrate CRUD technique. Parameter:  model_number import com.axeda.drm.sdk.Context import com.axeda.drm.sdk.device.ModelFinder import com.axeda.drm.sdk.device.Model import com.axeda.drm.sdk.device.DeviceFinder import com.axeda.drm.sdk.data.CurrentDataFinder import com.axeda.drm.sdk.device.Device import com.axeda.drm.sdk.data.HistoricalDataFinder import groovy.xml.MarkupBuilder import com.axeda.drm.sdk.device.DataItem import com.axeda.drm.services.device.DataItemType /* * DeleteDataItem.groovy * * Delete a data item. * * @param model_number        -   (REQ):Str name of the model. * * @author Sara Streeter <sstreeter@axeda.com> */ def response = [:] def writer = new StringWriter() def xml = new MarkupBuilder(writer) try { // getUserContext is supported as of release 6.1.5 and higher     final def CONTEXT = Context.getUserContext() // find the model     def modelFinder = new ModelFinder(CONTEXT)     modelFinder.setName(parameters.model_name)     Model model = modelFinder.findOne() // throw exception if no model found     if (!model) {         throw new Exception("No model found for ${parameters.model_name}.")     } // Add a dummy data item DataItem dataitem = new DataItem(CONTEXT, model, DataItemType.STRING, "MyDataItem"); dataitem.store(); // find the data items on the model model.dataItems.each{     logger.info(it.name)     if (it.name=="MyDataItem"){         it.delete()     } } } catch (def ex) {       xml.Response() {     Fault {           Code('Groovy Exception')           Message(ex.getMessage())           StringWriter sw = new StringWriter();           PrintWriter pw = new PrintWriter(sw);           ex.printStackTrace(pw);           Detail(sw.toString())         }       } } return ['Content-Type': 'text/xml', 'Content': writer.toString()]
View full tip
The following script takes a parameter of a model name, a device serial number and a data item name, finds the asset location and uses that longitude to determine the current TimeZone.  It then converts the Timezone of the data item timestamp to an Eastern Standard Timezone timestamp. import groovy.xml.MarkupBuilder import com.axeda.drm.sdk.Context import java.util.TimeZone import com.axeda.drm.sdk.data.* import com.axeda.drm.sdk.device.* import com.axeda.common.sdk.jdbc.*; import net.sf.json.JSONObject import net.sf.json.JSONArray import com.axeda.drm.sdk.mobilelocation.MobileLocationFinder import com.axeda.drm.sdk.mobilelocation.MobileLocation import com.axeda.drm.sdk.mobilelocation.CurrentMobileLocationFinder def response try {     Context ctx = Context.getUserContext()     ModelFinder mfinder = new ModelFinder(ctx)     mfinder.setName(parameters.model_name)     Model m = mfinder.find()     DeviceFinder dfinder = new DeviceFinder(ctx)     dfinder.setModel(m);     dfinder.setSerialNumber(parameters.device)     Device d = dfinder.find()     CurrentMobileLocationFinder cmlFinder = new CurrentMobileLocationFinder(ctx);     cmlFinder.setDeviceId(d.id.getValue());     MobileLocation ml = cmlFinder.find();     def lng = -72.158203125     if (ml?.lng){         lng = ml?.lng     }     // set boundaries for timezones - longitudes     def est = setUSTimeZone(-157.95415000000003)     def tz = setUSTimeZone(lng)     CurrentDataFinder cdfinder = new CurrentDataFinder(ctx, d)     DataValue dvalue = cdfinder.find(parameters.data_item_name)     def adjtime = convertToNewTimeZone(dvalue.getTimestamp(),tz,est)     def results = JSONObject.fromObject(lat: ml?.lat, lng: ml?.lng, current: [name: dvalue.dataItem.name, time: adjtime.format("MM/dd/yyyy HH:mm"), value: dvalue.asString()]).toString(2)     response = results } catch (Exception e) {     response = [                 message: "Error: " + e.message             ]     response =  JSONObject.fromObject(response).toString(2) } return ['Content-Type': 'application/json', 'Cache-Control':'no-cache', 'Content': response] def setUSTimeZone(lng){     TimeZone tz     // set boundaries for US timezones by longitude     if (lng <= -67.1484375 && lng > -85.517578125){         tz = TimeZone.getTimeZone("EST");     }     else if (lng <= -85.517578125 && lng > -96.591796875){         tz = TimeZone.getTimeZone("CST");     }     else if (lng <= -96.591796875 && lng > -113.90625){         tz = TimeZone.getTimeZone("MST");     }     else if (lng <= -113.90625){         tz = TimeZone.getTimeZone("PST");     }     logger.info(tz)     return tz } public Date convertToNewTimeZone(Date date, TimeZone oldTimeZone, TimeZone newTimeZone){     long oldDateinMilliSeconds=date.time - oldTimeZone.rawOffset     // oldtimeZone.rawOffset returns the difference(in milliSeconds) of time in that timezone with the time in GMT     // date.time returns the milliseconds of the date     Date dateInGMT=new Date(oldDateinMilliSeconds)     long convertedDateInMilliSeconds = dateInGMT.time + newTimeZone.rawOffset     Date convertedDate = new Date(convertedDateInMilliSeconds)     return convertedDate }
View full tip
The following script is a component of the Axeda Connected Configuration (CMDB) feature.  It is used to provide configuration data for controlling package deployments via Connected Content (SCM). ​ ConfigItem_CRU.groovy *Takes a POST request, not parameters import static com.axeda.sdk.v2.dsl.Bridges.* import com.axeda.drm.sdk.scripto.Request import com.axeda.services.v2.ConfigurationItem import com.axeda.services.v2.ConfigurationItemCriteria import com.axeda.services.v2.AssetConfiguration import com.axeda.services.v2.Asset import com.axeda.services.v2.ExecutionResult import groovy.json.JsonSlurper import net.sf.json.JSONObject import groovy.xml.MarkupBuilder /** * ConfigItem_CRU.groovy * ----------------------- * * Reads in json from an http post request and reads, adds, deletes or updates Configuration Items. * * * @note this parses a post and does not take any additional parameters. * * @author sara streeter <sstreeter@axeda.com> */ def contentType = "application/json" final def serviceName = "ConfigItem_CRU" def response = [:] def writer = new StringWriter() def xml = new MarkupBuilder(writer) try {     // BUSINESS LOGIC BEGIN     def assetId     def validationOnly     def validationResponse = ""     List<ConfigurationItem> configItemList     if (Request?.body != null && Request?.body !="") {         def slurper = new JsonSlurper()         def request = slurper.parseText(Request?.body)         assetId = request.result.assetId         validationOnly = request.result.validationOnly?.toBoolean()         if (request.result.items != null && request.result.items.size() > 0){             configItemList = request.result.items.inject([]) { target, item ->               if (item && item.path != "" && item.key != "" && item.path != null && item.key != null){                     ConfigurationItem configItem = new ConfigurationItem()                     configItem.path = item.path + item.key                     configItem.value = item.value                     target << configItem                 }                 target             }         }     }       if (assetId != null) {               def asset = assetBridge.find([assetId])[0]             AssetConfiguration config = assetConfigurationBridge.getAssetConfiguration(assetId, "")               def itemToDelete                        if (config == null) {                     createConfigXML(xml)                     AssetConfiguration configToCreate = assetConfigurationBridge.fromXml(writer.toString(), asset.id)                     ExecutionResult result = assetConfigurationBridge.create(configToCreate)                     AssetConfiguration config2 = assetConfigurationBridge.getAssetConfiguration(asset.id, "")                     config = config2                     itemToDelete = "/Item"                 }                 if (configItemList != null && configItemList?.size() > 0){                 List<ConfigurationItem> compareList = config.items                 def intersectingCompareItems = compareList.inject(["save": [], "delete": []]) { map, item ->                     // find whether to delete                     def foundItem = configItemList.findAll{ compare -> item?.path == compare?.path && item?.value == compare?.value  }                     map[foundItem.size() > 0 ? "save" : "delete"] << item                     map                 }               intersectingCompareItems.delete = intersectingCompareItems.delete.collect{it.path}               if (itemToDelete){                 intersectingCompareItems.delete.add(itemToDelete)               }                 def intersectingConfigItems = configItemList.inject(["old": [], "new": []]) { map, item ->                     // find whether it's old                     def foundItem = compareList.findAll{ compare -> item?.path == compare?.path && item?.value == compare?.value }                     map[foundItem.size() > 0 ? "old" : "new"] << item                     map                 }                 assetConfigurationBridge.deleteConfigurationItems(config, intersectingCompareItems.delete)                 assetConfigurationBridge.appendConfigurationItems(config, intersectingConfigItems.new)               def exResult = assetConfigurationBridge.validate(config)               if (exResult.successful){                     validationResponse = "success"                     if (!validationOnly){                         assetConfigurationBridge.update(config)                     }               }                 else {                     validationResponse = exResult.failures[0]?.details                 }             }             response = [                 assetId: assetId,                 items: config?.items?.collect { item ->                 def origpath = item.path                 def lastSlash = origpath.lastIndexOf("/")                 def key = origpath.substring(lastSlash + 1, origpath.length())                        def path = origpath.replace("/" + key, "")                 path += "/"                     [                         path: path,                         key: key,                         value: item.value                     ]                 },                 validationResponse: validationResponse             ]       }         else {             throw new Exception("Error: Asset Id must be provided.")         } } catch (Exception ex) {       logger.error ex   response = [           error:  [                   type: "Backend Application Error"                   , msg: ex.getLocalizedMessage()           ]   ] } return ['Content-Type': 'application/json', 'Content': JSONObject.fromObject(response).toString(2)] /** * Create the Success response. * * @param xml : The xml response.<br> * @param info : If this is set to "1" the info element will be included in the response.<br> * @param infos : Collection of information to include within the info element of the response.<br> */ private void createConfigXML(xml) {     xml.Item() }  
View full tip
This script illustrates how to call a Groovy script as an external web service.  This example also applies to calling any external web service that relies on a username and password. Parameters: external_username external_password script_name import com.axeda.drm.sdk.Context import com.axeda.drm.sdk.device.DeviceFinder import com.axeda.drm.sdk.data.CurrentDataFinder import com.axeda.drm.sdk.device.Device import com.axeda.drm.sdk.data.HistoricalDataFinder import com.axeda.drm.sdk.device.DataItem import net.sf.json.JSONObject import com.axeda.drm.sdk.device.ModelFinder import groovyx.net.http.* import static groovyx.net.http.ContentType.* import static groovyx.net.http.Method.* /** * CallScriptoAsExternalWebService.groovy * * This script illustrates how to call a Groovy script as an external web service. * * @param external_username       -   (REQ):Str Username for the external web service. * @param external_password       -   (REQ):Str Password for the external web service. * @param script_name             -   (REQ):Str Script Name to call. * * */ def result try { validateParameters(actual: parameters, expected: ["external_username", "external_password", "script_name"]) // authentication tokens (username + password) def auth_tokens = [username: parameters.external_username, password: parameters.external_password] http = new HTTPBuilder( 'http://platform.axeda.com/services/v1/rest/Scripto/execute/'+parameters.script_name ) // pass in dummy parameters to the script for illustration def parammap = [key1: "val1", key2: "val2"] // Call the script     http.request (GET, JSON) {       uri.query = auth_tokens + parammap       response.success = { resp, json ->         // traverse the wrapped json response     result = json.wsScriptoExecuteResponse.content.$          }       response.failure = { resp ->         result = response.failure       }      } } catch (Throwable any) {     logger.error any.localizedMessage } return ['Content-Type': 'application/json', 'Content': result] static def validateParameters(Map args) {     if (!args.containsKey("actual")) {         throw new Exception("validateParameters(args) requires 'actual' key.")     }     if (!args.containsKey("expected")) {         throw new Exception("validateParameters(args) requires 'expected' key.")     }     def config = [             require_username: false     ]     Map actualParameters = args.actual.clone() as Map     List expectedParameters = args.expected     config.each { key, value ->         if (args.options?.containsKey(key)) {             config[key] = args.options[key]         }     }     if (!config.require_username) { actualParameters.remove("username") }     expectedParameters.each { paramName ->         if (!actualParameters.containsKey(paramName) || !actualParameters[paramName]) {             throw new IllegalArgumentException(                     "Parameter '${paramName}' was not found in the query; '${paramName}' is a reqd. parameter.")         }     } }
View full tip
Calling external services from M2M applications is a critical aspect of building end-to-end solutions.  Knowing how to apply network timeouts when connecting to external servers can prevent unexpected and problematic network hang-ups. Let's investigate how to create a safe networking flow using HttpClient, HttpBuilder, and Apache’s FTPClient class. Background Custom Objects called from Expression Rules have a configurable maximum execution time.  This is set by the com.axeda.drm.rules.statistics.rule-time-threshold property.  Without this safeguard in place long running or misbehaved Custom Objects can cause internal processing queues to fill and the server will suffer a performance degradation. In Java (and Groovy) all network calls internally use InputStream.read() to establish the socket connection and to read data from the socket.  It is possible for faulty external servers (such as an FTP server) to hang and not properly respond.  This means that the InputStream.read() method will continuously wait for the server to respond with data, and the server will never respond.  According to the Java spec, InputStream.read() may be uninterruptable while it is waiting for data.  This means that if a Custom Object has exceeded the com.axeda.drm.rules.statistics.rule-time-threshold the Rule Sniper will still not be able to interrupt the Custom Object’s execution if it is waiting on InputStream.read().  Because the Custom Object cannot be stopped, the internal processing queues will eventually fill. Even though InputStream.read() is uninterruptable it is still possible to set timeouts for it to be able to give up on a connection.  Beyond that, we want to make sure that the connection is completely disconnected. Types of Timeouts There are typically two types of timeouts that should be set when making calls over the web: the Connection Timeout and the Socket Timeout.  The Connection Timeout is the maximum amount of time that should be allowed when establishing the bi-directional socket connection between the client and server.  Behind the scenes socket connection involves resolving the domain name of the server to an IP address, and then the server opening a port to connect with the client’s port.  The Socket Timeout is the timeout that limits the amount of time each socket operation is allowed to take.  It limits the amount of time InputStream.read() will listen for a server’s response.  If a server is faulty or overloaded it may take a long time (or forever) to respond to a request.  This timeout limits the amount of time the client will wait for the server to respond. When making any calls from a Custom Object to an external server (either making WebService calls, or FTP transfers), you should always set the Connection Timeout and the Socket Timeout.  Always try to keep the timeouts as reasonably small as possible.  Failure to do so could unexpectedly impact your Axeda server.  Consider a Custom Object that takes an average of 10 seconds to run is called to make an external WebService call once a minute. This will not cause any issues and the  system will be stable.  If the external server suddenly has a performance degredation and now the external WebService call takes over a minute to run, the execution queue will eventually fill, causing performance degradation to the Axeda system.  To protect against this scenario, set the timeouts to limit the call to one minute, and log whenever the time limit is exceeded. Examples Provided below are examples of properly set timeouts and thorough connection management use HttpClient, HttpBuilder, and FTPClient.  All of these examples assume they are being executed from Custom Objects. By default, set the Connection Timeout to 10 seconds.  In normal circumstances, connections should not take more then 10 seconds.  If they are exceeding this time there is a good chance of networking issues between the client and server. The Socket Timeout can vary per use-case.  The examples provided set the Socket Timeout to 30 seconds, which should be sufficient for typical WebService calls and small to medium sized FTP file transfers.  Depending exactly on what is being done, the timout may have to be increased.  If you expect the call to go over 5 minutes please contact Axeda Support to investigate increasing  com.axeda.drm.rules.statistics.rule-time-threshold property (which defaults to 5 minutes). ​HttpClient​ //HttpClient import org.apache.http.client.HttpClient import org.apache.http.impl.client.DefaultHttpClient import org.apache.http.client.methods.HttpGet import org.apache.http.HttpResponse import org.apache.http.params.BasicHttpParams import org.apache.http.params.HttpParams import org.apache.http.params.HttpConnectionParams int TENSECONDS  = 10*1000 int THIRTYSECONDS = 30*1000 final HttpParams httpParams = new BasicHttpParams() //Establishing the connection should take <10 seconds in most circumstances HttpConnectionParams.setConnectionTimeout(httpParams, TENSECONDS) //The data transfer/call should take <30 seconds.  Adjust as necessary if receiving large data sets. HttpConnectionParams.setSoTimeout(httpParams, THIRTYSECONDS) HttpClient hc = new DefaultHttpClient(httpParams) try {   //Simply get the contents of http://www.axeda.com and log it to the Custom Object Log   HttpGet get = new HttpGet("http://www.axeda.com")   HttpResponse response = hc.execute(get)   BufferedReader br = new BufferedReader( new InputStreamReader( response.getEntity().getContent()))   br.readLines().each {     logger.info it   } } finally {   //Make sure to shutdown the connectionManager   hc.getConnectionManager().shutdown() } return true https://gist.github.com/axeda/5189092/raw/2f7b93c5f96ed8f445df4364b885486bc6fa1feb/HttpClientTimeouts.groovy HttpBuilder import groovyx.net.http.HTTPBuilder import static groovyx.net.http.ContentType.* import static groovyx.net.http.Method.* int TENSECONDS  = 10*1000; int THIRTYSECONDS = 30*1000; HTTPBuilder builder = new HTTPBuilder('http://www.axeda.com') //HTTPBuilder has no direct methods to add timeouts.  We have to add them to the HttpParams of the underlying HttpClient builder.getClient().getParams().setParameter("http.connection.timeout", new Integer(TENSECONDS)) builder.getClient().getParams().setParameter("http.socket.timeout", new Integer(THIRTYSECONDS)) try {   //Simply get the contents of http://www.axeda.com and log it to the Custom Object Log   builder.request(GET, TEXT){     response.success = { resp, res ->       res.readLines().each {         logger.info it       }       }   } } finally {   //Make sure to always shut down the HTTPBuilder when you’re done with it   builder.shutdown() } return true https://gist.github.com/axeda/5189102/raw/66bb3a4f4f096681847de1d2d38971e6293c4c6b/HttpBuilderTimeouts.groovy FtpClient Apache’s FTPClient has a third type of timeout, the Default Timeout.  The Default Timeout is a timeout that further ensures that socket timeouts are always used.  Note: Default Timeout does not set a timeout for the .connect() method. import org.apache.commons.net.ftp.* import java.io.InputStream import java.io.ByteArrayInputStream String ftphost = "127.0.0.1" String ftpuser = "test" String ftppwd = "test" int ftpport = 21 String ftpDir = "tmp/FTP" int TENSECONDS  = 10*1000 int THIRTYSECONDS = 30*1000 //Declare FTP client FTPClient ftp = new FTPClient() try {   ftp.setConnectTimeout(TENSECONDS)   ftp.setDefaultTimeout(TENSECONDS)   ftp.connect(ftphost, ftpport)   //30 seconds to log on.  Also 30 seconds to change to working directory.   ftp.setSoTimeout(THIRTYSECONDS)   def reply = ftp.getReplyCode()   if (!FTPReply.isPositiveCompletion(reply))   {     throw new Exception("Unable to connect to FTP server")   }   if (!ftp.login(ftpuser, ftppwd))   {     throw new Exception("Unable to login to FTP server")   }   if (!ftp.changeWorkingDirectory(ftpDir))   {     throw new Exception("Unable to change working directory on FTP server")   }   //Change the timeout here for a large file transfer that will take over 30 seconds   //ftp.setSoTimeout(THIRTYSECONDS);   ftp.setFileType(FTPClient.ASCII_FILE_TYPE)   ftp.enterLocalPassiveMode()   String filetxt = "Some String file content"   InputStream is = new ByteArrayInputStream(filetxt.getBytes('US-ASCII'))   try   {     if (!ftp.storeFile("myFile.txt", is))     {       throw new Exception("Unable to write file to FTP server")     }   } finally   {     //Make sure to always close the inputStream     is.close()   } } catch(Exception e) {   //handle exceptions here by logging or auditing } finally {   //if the IO is timed out or force disconnected, exceptions may be thrown when trying to logout/disconnect   try   {     //10 seconds to log off.  Also 10 seconds to disconnect.     ftp.setSoTimeout(TENSECONDS);     ftp.logout();     //depending on the state of the server the .logout() may throw an exception,     //we want to ensure complete disconnect.   }   catch(Exception innerException)   {       //You potentially just want to log that there was a logout exception.     }   finally   {     //Make sure to always disconnect.  If not, there is a chance you will leave hanging sockects     ftp.disconnect();   } } return true https://gist.github.com/axeda/5189120/raw/83545305a38d03b6a73a80fbf4999be3d6b3e74e/FtpClientConnectionTimeouts.groovy
View full tip
This script creates a csv file from the audit log filtered by the User Access category, so dates of when users logged in or logged out. *** see update below *** Note:  The csv file has the same name as the Groovy script and does NOT have the .csv extension . To get the .csv extension, the Groovy script has to be renamed to AuditEntryToCSV.csv.groovy .  Suggestions on how to improve this are welcome. *** Update ***: The download works without the renamed groovy script by returning text instead of an input stream.  The script has been modified to illustrate this. Parameters: days - the number of days past to fetch audit logs model_name - the model name of the asset serial_number - the serial number of the asset import com.axeda.drm.sdk.device.ModelFinder import com.axeda.drm.sdk.Context import com.axeda.common.sdk.id.Identifier import com.axeda.drm.sdk.device.Model import com.axeda.drm.sdk.device.DeviceFinder import com.axeda.drm.sdk.device.Device import com.axeda.drm.sdk.audit.AuditCategoryList import com.axeda.drm.sdk.audit.AuditCategory import com.axeda.drm.sdk.audit.AuditEntryFinder import com.axeda.drm.sdk.audit.SortType import com.axeda.drm.sdk.audit.AuditEntry import groovy.xml.MarkupBuilder import com.axeda.platform.sdk.v1.services.ServiceFactory /* * AuditEntryToCSV.groovy * * Creates a csv file from the audit log filtered by the User Access category, so dates of when users logged in or logged out. * * @param days        -   (REQ):Str number of days to search. * @param model_name        -   (REQ):Str name of the model. * @param serial_number        -   (REQ):Str serial number of the device. * * @note - the csv file has the same name as the Groovy script and does NOT have the .csv extension . To get * the .csv extension, the Groovy script has to be renamed to AuditEntryToCSV.csv.groovy . * * @author Sara Streeter <sstreeter@axeda.com> */ def writer = new StringWriter() def xml = new MarkupBuilder(writer) try {    def ctx = Context.getUserContext()    ModelFinder modelFinder = new ModelFinder(ctx)    modelFinder.setName(parameters.model_name)    Model model = modelFinder.find()    DeviceFinder deviceFinder = new DeviceFinder(ctx)    deviceFinder.setSerialNumber(parameters.serial_number)    Device device = deviceFinder.find()    AuditCategoryList acl = new AuditCategoryList()    acl.add(AuditCategory.USER_ACCESS)    long now = System.currentTimeMillis()    Date today = new Date(now)    def paramdays = parameters.days ? parameters.days: 5    long days = 1000 * 60 * 60 * 24 * Integer.valueOf(paramdays)    AuditEntryFinder aef = new AuditEntryFinder(ctx)    aef.setCategories(acl)    aef.setToDate(today)    aef.setFromDate(new Date(now - (days)))    aef.setSortType(SortType.DATE)    aef.sortDescending()    List<AuditEntry> audits = aef.findAll() // use a Data Accumulator to store the information def dataStoreIdentifier = "FILE-CSV-audit_log" def daSvc = new ServiceFactory().dataAccumulatorService if (daSvc.doesAccumulationExist(dataStoreIdentifier, device.id.value)) {     daSvc.deleteAccumulation(dataStoreIdentifier, device.id.value) } // assemble the response    audits.each { AuditEntry audit ->            def row = [                audit?.id.value,                audit?.user?.username,                audit?.date,                audit?.category?.bundleKey,                audit?.message            ]         row = row.join(',')         row += '\n'         daSvc.writeChunk(dataStoreIdentifier, device.id.value, row);        } // stream the data accumulator to create the file    InputStream is = daSvc.streamAccumulation(dataStoreIdentifier, device.id.value) return ['Content-Type': 'text/csv', 'Content-Disposition':'attachment; filename=AuditEntryCSVFile.csv', 'Content': is.text] } catch (def ex) {    xml.Response() {        Fault {            Code('Groovy Exception')            Message(ex.getMessage())            StringWriter sw = new StringWriter();            PrintWriter pw = new PrintWriter(sw);            ex.printStackTrace(pw);            Detail(sw.toString())        }    } logger.info(writer.toString()) }
View full tip
This script finds all the data items both current and historical on all the assets of a model and outputs them as XML. Parameters: model_name from_time to_time import com.axeda.drm.sdk.Context import com.axeda.drm.sdk.device.ModelFinder import com.axeda.drm.sdk.device.Model import com.axeda.drm.sdk.device.DeviceFinder import com.axeda.drm.sdk.data.CurrentDataFinder import com.axeda.drm.sdk.device.Device import com.axeda.drm.sdk.data.HistoricalDataFinder import groovy.xml.MarkupBuilder /* * AllDataItems2XML.groovy * * Find all the historical and current data items for all assets in a given model. * * @param model_name        -   (REQ):Str name of the model. * @param from_time         -   (REQ):Long millisecond timestamp to begin query from. * @param to_time           -   (REQ):Long millisecond timestamp to end query at. * * @note from_time and to_time should be provided because it limits the query size. * * @author Sara Streeter <sstreeter@axeda.com> */ def response = [:] def writer = new StringWriter() def xml = new MarkupBuilder(writer) // measure the script run time def timeProfiles = [:] def scriptStartTime = new Date() try { // getUserContext is supported as of release 6.1.5 and higher     final def CONTEXT = Context.getUserContext() // confirm that required parameters have been provided     validateParameters(actual: parameters, expected: ["model_name", "from_time", "to_time"]) // find the model     def modelFinder = new ModelFinder(CONTEXT)     modelFinder.setName(parameters.model_name)     Model model = modelFinder.findOne() // throw exception if no model found     if (!model) {         throw new Exception("No model found for ${parameters.model_name}.")     } // find all assets of that model     def assetFinder = new DeviceFinder(CONTEXT)     assetFinder.setModel(model)     def assets = assetFinder.findAll() // find the current and historical data values for each asset //note: since device will be set on the datafinders going forward, a dummy device is set on instantiation which is not actually stored     def currentDataFinder = new CurrentDataFinder(CONTEXT, new Device(CONTEXT, "placeholder", model))     def historicalDataFinder = new HistoricalDataFinder(CONTEXT, new Device(CONTEXT, "placeholder", model))     historicalDataFinder.startDate = new Date(parameters.from_time as Long)     historicalDataFinder.endDate = new Date(parameters.to_time as Long) // assemble the response     xml.Response(){         assets.each { Device asset ->             currentDataFinder.device = asset             def currentValueList = currentDataFinder.find()             historicalDataFinder.device = asset             def valueList = historicalDataFinder.find()             Asset(){                     id(asset.id.value)                     name( asset.name)                     serial_number(asset.serialNumber)                     model_id( asset.model.id.value)                     model_name(asset.model.name)                     current_data(){                         currentValueList.each{ data ->                         timestamp( data?.getTimestamp()?.format("yyyyMMdd HH:mm"))                          name(data?.dataItem?.name)                          value( data?.asString())                     }}                     historical_data(){                         valueList.each { data ->                         timestamp( data?.getTimestamp()?.format("yyyyMMdd HH:mm"))                          name(data?.dataItem?.name)                          value( data?.asString())                     }}             }         }     } } catch (def ex) {       xml.Response() {     Fault {           Code('Groovy Exception')           Message(ex.getMessage())           StringWriter sw = new StringWriter();           PrintWriter pw = new PrintWriter(sw);           ex.printStackTrace(pw);           Detail(sw.toString())         }       } } return ['Content-Type': 'text/xml', 'Content': writer.toString()] private Map createTimeProfile(String label, Date startTime, Date endTime) {     [             (label): [                     startTime: [timestamp: startTime.time, readable: startTime.toString()],                     endTime: [timestamp: endTime.time, readable: endTime.toString()],                     profile: [                             elapsed_millis: endTime.time - startTime.time,                             elapsed_secs: (endTime.time - startTime.time) / 1000                     ]             ]     ] } private validateParameters(Map args) {     if (!args.containsKey("actual")) {         throw new Exception("validateParameters(args) requires 'actual' key.")     }     if (!args.containsKey("expected")) {         throw new Exception("validateParameters(args) requires 'expected' key.")     }     def config = [             require_username: false     ]     Map actualParameters = args.actual.clone() as Map     List expectedParameters = args.expected     config.each { key, value ->         if (args.options?.containsKey(key)) {             config[key] = args.options[key]         }     }     if (!config.require_username) { actualParameters.remove("username") }     expectedParameters.each { paramName ->         if (!actualParameters.containsKey(paramName) || !actualParameters[paramName]) {             throw new IllegalArgumentException(                     "Parameter '${paramName}' was not found in the query; '${paramName}' is a reqd. parameter.")         }     } } Sample Output: <Response>   <Asset>   <id>2864</id>   <name>keg24</name>   <serial_number>keg24</serial_number>   <model_id>1081</model_id>   <model_name>Kegerator</model_name>   <current_data>   <timestamp>20111103 14:44</timestamp>   <name>currKegPercentage</name>   <value>34.0</value>   <timestamp>20111103 14:38</timestamp>   <name>currTempF</name>   <value>43.0</value>   </current_data>   <historical_data />   </Asset>   <Asset>   <id>2861</id>   <name>keg28</name>   <serial_number>keg28</serial_number>   <model_id>1081</model_id>   <model_name>Kegerator</model_name>   <current_data>   <timestamp />   <name>currKegPercentage</name>   <value>?</value>   <timestamp>20111103 14:21</timestamp>   <name>currTempF</name>   <value>43.0</value>   </current_data>   <historical_data />   </Asset>   <Asset>   <id>2863</id>   <name>keg21</name>   <serial_number>keg21</serial_number>   <model_id>1081</model_id>   <model_name>Kegerator</model_name>   <current_data>   <timestamp />   <name>currKegPercentage</name>   <value>?</value>   <timestamp>20111103 14:39</timestamp>   <name>currTempF</name>   <value>42.0</value>   </current_data>   <historical_data />   </Asset>   <Asset>   <id>2862</id>   <name>keg25</name>   <serial_number>keg25</serial_number>   <model_id>1081</model_id>   <model_name>Kegerator</model_name>   <current_data>   <timestamp>20111103 14:36</timestamp>   <name>currKegPercentage</name>   <value>34.0</value>   <timestamp />   <name>currTempF</name>   <value>?</value>   </current_data>   <historical_data />   </Asset>   <Asset>   <id>2867</id>   <name>keg29</name>   <serial_number>keg29</serial_number>   <model_id>1081</model_id>   <model_name>Kegerator</model_name>   <current_data>   <timestamp>20111103 14:48</timestamp>   <name>currKegPercentage</name>   <value>35.0</value>   <timestamp />   <name>currTempF</name>   <value>?</value>   </current_data>   <historical_data />   </Asset>   <Asset>   <id>2865</id>   <name>keg27</name>   <serial_number>keg27</serial_number>   <model_id>1081</model_id>   <model_name>Kegerator</model_name>   <current_data>   <timestamp>20111103 14:39</timestamp>   <name>currKegPercentage</name>   <value>34.0</value>   <timestamp>20111103 14:44</timestamp>   <name>currTempF</name>   <value>42.0</value>   </current_data>   <historical_data />   </Asset>   <Asset>   <id>2866</id>   <name>keg23</name>   <serial_number>keg23</serial_number>   <model_id>1081</model_id>   <model_name>Kegerator</model_name>   <current_data>   <timestamp>20111103 14:46</timestamp>   <name>currKegPercentage</name>   <value>34.0</value>   <timestamp />   <name>currTempF</name>   <value>?</value>   </current_data>   <historical_data />   </Asset> </Response>
View full tip
Applicable Releases: ThingWorx Platform 7.0 to 8.5   Description:   Main concepts and best practices for devops methodology such as Naming Conventions Setup and management of environments for development and testing Import/Export process and application deployment Use of Tags and Project to control your development Coding Standards Validation best practices         For project packaging and deployment, make sure to check the content about Solution Central created after this session was released
View full tip
This small tutorial enables you to manage payload decoding for Adeunis Devices within ThingWorx Composer in less than 10 minutes.  Adeunis Devices communicates on LPWAN networks (LoRaWAN / Sigfox) covering sectors such as smart building, smart industry and smart city. The encoding is also possible but it will be covered in another article.   1. Get Adeunis Codec Adeunis is providing a codec enabling payload encoding and decoding.  Download here the resource file containing the codec.  Unzip the file and edit "script.txt" with your favorite text editor. Copy all text contained in the file.   2.  Create AdeunisCodec Thing Create a Thing called "AdeunisCodec" based on the GenericThing Template.   3. Create a service called "Decode" Create a Decode Service with the following setup: Inputs: type (String), payload (String) Output as JSON Past the previously copied "script.txt" content Save   4. Correct a couple of Warnings Remove all "var codec;" occurences except first one at line 1191.  Remove semi columns at lines 985,1088, 1096 and 1172   5. Remove the following section The codec relies on implementing functions on JavaScript prototypes which is not supported by ThingWorx Rhino JavaScript Engine. See the following documentation section, here.    Remove from line 1109 to 1157.   The following classes overrides will be removed: Uint8Array.prototype.readUInt16BE Uint8Array.prototype.readInt16BE Uint8Array.prototype.readUInt8 Uint8Array.prototype.readUInt32BE Uint8Array.prototype.writeUInt16BE Uint8Array.prototype.writeUInt8 Uint8Array.prototype.writeUInt32BE 6. Add new implementations of the removed functions The functions are adapted from a JavaScript framework which contains resources that helps dealing with binary data, here. Insert the  following section at the top of the "Decode" script.         function readInt16BE (payload,offset) { checkOffset(offset, 2, payload.length); var val = payload[offset + 1] | (payload[offset] << 8); return (val & 0x8000) ? val | 0xFFFF0000 : val; } function readUInt32BE (payload,offset) { checkOffset(offset, 4, payload.length); return (payload[offset] * 0x1000000) + ((payload[offset + 1] << 16) | (payload[offset + 2] << | payload[offset + 3]); } function readUInt16BE (payload,offset) { checkOffset(offset, 2, payload.length); return (payload[offset] << | payload[offset + 1]; } function readUInt8 (payload,offset) { checkOffset(offset, 1, payload.length); return payload[offset]; } function writeUInt16BE (payload,value, offset) { value = +value; offset = offset >>> 0; checkInt(payload, value, offset, 2, 0xffff, 0); if (Buffer.TYPED_ARRAY_SUPPORT) { this[offset] = (value >>> 8); payload[offset + 1] = value; } else objectWriteUInt16(payload, value, offset, false); return offset + 2; } function writeUInt8 (payload,value, offset) { value = +value; offset = offset >>> 0; checkInt(payload, value, offset, 1, 0xff, 0); if (!Buffer.TYPED_ARRAY_SUPPORT) value = Math.floor(value); payload[offset] = value; return offset + 1; } function writeUInt32BE (payload,value, offset) { value = +value; offset = offset >>> 0; checkInt(payload, value, offset, 4, 0xffffffff, 0); if (Buffer.TYPED_ARRAY_SUPPORT) { payload[offset] = (value >>> 24); payload[offset + 1] = (value >>> 16); payload[offset + 2] = (value >>> 8); payload[offset + 3] = value; } else objectWriteUInt32(payload, value, offset, false); return offset + 4; } function objectWriteUInt16 (buf, value, offset, littleEndian) { if (value < 0) value = 0xffff + value + 1; for (var i = 0, j = Math.min(buf.length - offset, 2); i < j; i++) { buf[offset + i] = (value & (0xff << (8 * (littleEndian ? i : 1 - i)))) >>> (littleEndian ? i : 1 - i) * 8; } } function objectWriteUInt32 (buf, value, offset, littleEndian) { if (value < 0) value = 0xffffffff + value + 1; for (var i = 0, j = Math.min(buf.length - offset, 4); i < j; i++) { buf[offset + i] = (value >>> (littleEndian ? i : 3 - i) * & 0xff; } }     7. Add the following function to support previous inserted functions     function checkOffset (offset, ext, length) { if ((offset % 1) !== 0 || offset < 0) throw new Error ('offset is not uint'); if (offset + ext > length) throw new Error ('Trying to access beyond buffer length'); }     8. Add the following function for casting String to Bytes     function splitInBytes(data) { var bytes = []; var bytesAsString = ''; for (var i = 0, j = 0; i < data.length; i += 2, j++) { bytes[j] = parseInt(data.substr(i, 2), 16); bytesAsString += bytes[j] + ' '; } return bytes; }     9. Remap function calls to newly inserted functions Use the built-in script editor replace feature for the following, see below:   Within the service script perform a Replace for each of the following lines. Search Replace by payload.readInt16BE( readInt16BE(payload, payload.readUInt32BE( readUInt32BE(payload, payload.readUInt16BE( readUInt16BE(payload, payload.readUInt8( readUInt8(payload, payload.writeUInt16BE( writeUInt16BE(payload, payload.writeUInt8( writeUInt8(payload, payload.writeUInt32BE( writeUInt32BE(payload,   10. At the Bottom update the following Replace : decoder.setDeviceType("temp"); By : decoder.setDeviceType(type);   11. Insert the following at the bottom var result = Decoder(splitInBytes(payload), 0);   12. Save Service and Thing   13. Create a test Service for Adeunis Temp Device Within "AdeunisCodec" Thing Create a new service called "test_decode_temp" with Output as String Insert the following code:      // result: STRING var result = me.Decode({type: "temp" /* STRING */,payload: "43400100F40200F1" /* STRING */});     Save & Execute  The expected result is:     {"temperatures":[{"unit":"°C","name":"probe 1","id":0,"value":24.4},{"unit":"°C","name":"probe 2","id":0,"value":24.1}],"type":"0x43 Temperature data","status":{"frameCounter":2,"lowBattery":false,"hardwareError":false,"probe1Alarm":false,"configurationDone":false,"probe2Alarm":false}}       Please visit the Decoder test section of Adeunis website to see the reference for the Temp device test case, here.   Spoiler (Highlight to read) The resources has been tested on ThingWorx 8.5 and with the latest and greatest ThingWorx 9...   If you are more interested in the result than in the implementation process then import the attached "Things_AdeunisCodec.xml" 😉  The resources has been tested on ThingWorx 8.5 and with the latest and greatest ThingWorx 9...  If you are more interested in the result than in the implementation process then import the attached "Things_AdeunisCodec.xml"    
View full tip
Recently I needed to be able to parse and handle XML data natively inside of a ThingWorx script, and this XML file happened to have a SOAP namespace as well. I learned a few things along the way that I couldn’t find a lot of documentation on, so am sharing here.   Lessons Learned The biggest lesson I learned is that ThingWorx uses “E4X” XML handling. This is a language that Mozilla created as a way for JavaScript to handle XML (the full name is “ECMAscript for XML”). While Mozilla deprecated the language in 2014, Rhino, the JavaScript engine that ThingWorx uses on the server, still supports it, so ThingWorx does too. Here’s a tutorial on E4X - https://developer.mozilla.org/en-US/docs/Archive/Web/E4X_tutorial The built-in linter in ThingWorx will complain about E4X syntax, but it still works. I learned how to get to the data I wanted and loop through to create an InfoTable. Hopefully this is what you want to do as well.   Selecting an Element and Iterating My data came inside of a SOAP envelope, which was meaningless information to me. I wanted to get down a few layers. Here’s a sample of my data that has made-up information in place of the customer's original data:                <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" headers="">     <SOAP-ENV:Body>         <get_part_schResponse xmlns="urn:schemas-iwaysoftware-com:iwse">             <get_part_schResult>                 <get_part_schRow>                     <PART_NO>123456</PART_NO>                     <ORD_PROC_DIV_CD>E</ORD_PROC_DIV_CD>                     <MFG_DIV_CD>E</MFG_DIV_CD>                     <SCHED_DT>2020-01-01</SCHED_DT>                 </get_part_schRow>                 <get_part_schRow>                     <PART_NO>789456</PART_NO>                     <ORD_PROC_DIV_CD>E</ORD_PROC_DIV_CD>                     <MFG_DIV_CD>E</MFG_DIV_CD>                     <SCHED_DT>2020-01-01</SCHED_DT>                 </get_part_schRow>             </get_part_schResult>         </get_part_schResponse>     </SOAP-ENV:Body> </SOAP-ENV:Envelope> To get to the schRow data, I need to get past SOAP and into a few layers of XML. To do that, I make a new variable and use the E4X selections to get there: var data = resultXML.*::Body.*::get_part_schResponse.*::get_part_schResult.*; Note a few things: resultXML is a variable in the service that contains the XML data. I skipped the Envelope tag since that’s the root. The .* syntax does not mean “all the following”, it means “all namespaces”. You can define and specify the namespaces instead of using .*, but I didn’t find value in that. I found some sample code that theoretically should work on a VMware forum: https://communities.vmware.com/thread/592000. This gives me schRow as an XML List that I can iterate through. You can see what you have at this point by converting the data to a String and outputting it: var result = String(data); Now that I am to the schRow data, I can use a for loop to add to an InfoTable: for each (var row in data) {      result.AddRow({         PartNumber: row.*::PART_NO,         OrderProcessingDivCD: row.*::ORD_PROC_DIV_CD,         ManufacturingDivCD: row.*::MFG_DIV_CD,         ScheduledDate: row.*::SCHED_DT     }); } Shoo! That’s it! Data into an InfoTable! Next time, I'll ask for a JSON API. 😊
View full tip
Hi all,   ThingWorx contains lots of useful functionality for your services (last count is 339 Snippets in ThingWorx 8.5.2). These snippets are an important part of the platform application building capabilities, and most of them are simple enough to understand based on their name and the description that appears when hovering on them.   I have witnessed that however, in some cases, the platform users are not aware of their full capabilities. With this in mind, I started creating some time ago a Snippet Guide for my personal use that I'm sharing now with the community. It contains additional explanations, documentation links and sample source code tested by me.   Please bear in mind that it was done for an earlier ThingWorx version and I did not have enough time to update it for 8.5.x, but it should work the same here as well.   This enhanced documentation is not supported by PTC, so please 1. do not open a Tech Support ticket based on the content of this document and, instead 2. Comment on this thread if there are things I can improve on it.   Happy New Year!
View full tip
I'm getting up to speed on all the great new stuff in 8.5, and have found that since the JavaScript engine was upgraded to Rhino 1.7.11, there's some awesome new JavaScript ES6 functionality available. I have tested arrow functions, filter, map, and reduce. Compose does not look like it is supported.   If you're not familiar with this functionality, I highly recommend reading up on them. Filter, map, and reduce are incredibly useful for working with arrays. They can save you a lot of annoying logic.   Here's some resources that I've found helpful for learning: JavaScript Functional Programming - map, filter and reduce Arrow Functions: Fat and Concise Syntax in Javascript If you really want to dive into ES6, Wes Bos has incredible tutorial sessions that are worth every penny: Wes Bos: ES6 for Everyone!   Have you played around with ES6 functionality in ThingWorx 8.5 yet?
View full tip
Here is a tutorial to explain the process of uploading a PMML file from an external system to Thingworx Analytics. The tutorial steps are explained in the attached PDF and all referenced files can be found in the attached ZIP.  
View full tip
Let's assume I collect Timeseries Data of two temperature sensors, located next to each other. This is done for redundancy and ensuring the quality of measures. Each of the sensors is logged into its Property in ThingWorx and I can create a Timeseries for the individual sensors. However I would like to create a combined InfoTable that holds information for both sensors, but averages out their values.   Instead of reading values from a stream, I just create some custom data for both InfoTables. After this I use the UNION function to combine the two tables and sort them. Once they are sorted, the INTERPOLATE function allows to group the InfoTable by timestamp.   With this, I have combined the two sensor result into on result set. Taking the average of numbers will give closer results to the real value (as both sensors might not be 100% accurate). In case one sensor does not have data for a given point in time, it will still be considered in the final output.   InfoTable1:   2018-12-18 00:00:00.000 2 2018-12-19 00:00:00.000 3 2018-12-20 00:00:00.000 5 2018-12-21 00:00:00.000 7   InfoTable2:   2018-12-18 00:00:00.000 1 2018-12-19 12:00:00.000 2 2018-12-20 00:00:00.000 3 2018-12-21 00:00:00.000 4   Combined Result:   2018-12-18 00:00:00.000 1.5 2018-12-19 00:00:00.000 3 2018-12-19 12:00:00.000 2 2018-12-20 00:00:00.000 4 2018-12-21 00:00:00.000 5.5     This can be done with the following code:   // Required DataShape "myInfoTableShape": "timestamp" = DATETIME, "value" = NUMBER // The Service Output is an InfoTable based on the same DataShape var params = { infoTableName : "InfoTable", dataShapeName : "myInfoTableShape" }; // Create two InfoTables, representing the data of each sensor var infoTable1 = Resources["InfoTableFunctions"].CreateInfoTableFromDataShape(params); var infoTable2 = Resources["InfoTableFunctions"].CreateInfoTableFromDataShape(params); var newEntry = new Object(); // Create custom data for InfoTable1 newEntry.timestamp = 1545091200000; newEntry.value = 2; infoTable1.AddRow(newEntry); newEntry.timestamp = 1545177600000; newEntry.value = 3; infoTable1.AddRow(newEntry); newEntry.timestamp = 1545264000000; newEntry.value = 5; infoTable1.AddRow(newEntry); newEntry.timestamp = 1545350400000; newEntry.value = 7; infoTable1.AddRow(newEntry); // Create custom data for InfoTable2 newEntry.timestamp = 1545091200000; newEntry.value = 1; infoTable2.AddRow(newEntry); newEntry.timestamp = 1545220800000; newEntry.value = 2; infoTable2.AddRow(newEntry); newEntry.timestamp = 1545264000000; newEntry.value = 3; infoTable2.AddRow(newEntry); newEntry.timestamp = 1545350400000; newEntry.value = 4; infoTable2.AddRow(newEntry); // Combine the two InfoTables via the UNION function var unionTable = Resources["InfoTableFunctions"].Union({ t1: infoTable1, t2: infoTable2 }); // Optional: Sort the table by timestamp var sortedTable = Resources["InfoTableFunctions"].Sort({ sortColumn: "timestamp", t: unionTable, ascending: true }); // Interpolate the (sorted) table by Interval and take average values and build the result var result = Resources["InfoTableFunctions"].Interpolate({ mode: "INTERVAL", timeColumn: "timestamp", t: sortedTable, ignoreMissingData: undefined, stats: "AVG", endDate: 1545609600000, columns: "value", count: undefined, startDate: 1545004800000 });  
View full tip
I have created a mashup which allows you to easily use and test the Prescriptions functionality in Thingworx Analytics (TWA). This is where you choose 1 or more fields for optimization, and TWA tells you how to adjust those fields to get an optimal outcome.   The functionality is based on a public sample dataset for concrete mixtures, full details are included in the attached documentation.  
View full tip
Several times in the past few months I was hit by a quick need to extract some data about Assets for a customer, and find myself continually hand-writing the code to do so.  Rather than repeat myself any more, I figure I can share my work - maybe PTC customers can benefit from the same effort.    import static com.axeda.sdk.v2.dsl.Bridges.* import com.axeda.drm.sdk.Context import com.axeda.common.sdk.id.Identifier import com.axeda.services.v2.* import com.axeda.sdk.v2.exception.* def retStr = "Device and Location Data\n" def modellist = [:] ModelCriteria mc = new ModelCriteria() mc.modelNumber = "*" tcount = 0 def mresults = modelBridge.find(mc) while ( (mresults = modelBridge.find(mc)) != null && tcount < mresults .totalCount) { mresults.models.each { res -> modellist[res.systemId] = res.modelNumber tcount++ } mc.pageNumber = mc.pageNumber + 1 } locationList = [:] LocationCriteria lc = new LocationCriteria() lc.name = "*" tcount = 0 def lresults = locationBridge.find(lc) while ( (lresults = locationBridge.find(lc)) != null && tcount < lresults .totalCount) { lresults.locations.each { res -> locationList[res.systemId] = res.name tcount++ } lc.pageNumber = lc.pageNumber + 1 } AssetCriteria ac = new AssetCriteria() ac.includeDetails = true ac.name = "*" tcount = 0 def results = assetBridge.find(ac) while ( (results = assetBridge.find(ac)) != null && tcount < results .totalCount) { results.assets.each { res -> retStr += "ID: ${res.systemId} MN: ${res.model.systemId},${modellist[res.model.systemId]} SN: ${res.serialNumber} Name: ${res.name} : Location ${res.location.systemId},${locationList[res.location.systemId]}\n"; tcount++ } ac.pageNumber = ac.pageNumber + 1 } return ["Content-Type": "application/text", "Content": retStr] This will output data like so:    ID: 31342 MN: 14682,CKGW SN: Axeda-CK-Windows10VBox Name: Axeda-CK-Windows10VBox : Location 1,Foxboro ID: 26248 MN: 14682,CKGW SN: CK-CKAMINSKI0L1 Name: CK-CKAMINSKI0L1 : Location 1,Foxboro ID: 30082 MN: 14682,CKGW SN: CK-GW1 Name: CK-GW1 : Location 1,Foxboro ID: 26247 MN: 14681,CKGW-ManagedModel1 SN: CK-MM01 Name: CK-MM01 : Location 1,Foxboro This let's me compare the internal systemId of the Asset, the internal systemId of the Model, and the internal systemId of the Location of the device.  This was to help me attempt to isolate an issue with orphaned devices not being returned in a report - exposing some duplicate locations and devices that needed corrections.    You may find yourself needing to do similar things when building logic for Axeda, or eventually integrating or migrating to Thingworx.  Our v2 API bridges help "bridge" the gap.      
View full tip
To help explain some of the different ways in which a prediction can be triggered from a Thingworx Analytics Model, I've built a mashup which allows you to easily trigger these types of prediction:   - API Realtime Prediction - Analytics Manager: Event - API Batch Prediction   For information on setting up this environment to use the mashup with some sample data, please see the attached instructions document: Prediction-Methods-Mashup.pdf. The referenced resource files can be found inside resources.zip   For more information on prediction scoring please see this related post: How to score new data with ThingWorx Analytics 8.3.x
View full tip
Prerequisite Install Go Install VSCode or desired IDE to write Go code, e.g. GoLand (commercial license required, 30days trial) Install Go extension for VSCode (if you are working with VSCode)   Content Building GET Request Building PUT Request Building POST Request   Building GET Request I'll be using net/http package from Go to perform the GET request to the ThingWorx Server by importing it   import (     "net/http" ) Next, we use the NewRequest() which takes method, URL & body. Since I'm sending a GET request my method will be GET, and the URL to the ThingWorx server & no body so will leave it to nil     url := myurl req, _ := http.NewRequest("GET", url, nil)   We are ignoring the error that NewRequest is returning as its already handled within the NewRequest() for us Use Header to add the request header to be received by the ThingWorx Server, note Header is of type map[string] []string (a key : value pair)     req.Header.Add("appKey", appkey) // passing the appkey from ThingWorx Server for authentication req.Header.Add("Accept", "application/json") // accepts json as response req.Header.Add("Cache-Control", "no-cache") // not using cache to fetch data Now we invoke the DefaultClient to perform the request & handling the error res, err := http.DefaultClient.Do(req)     if err != nil {         log.Println("Failed to get all entity list from the server", err)     } We need to close the body once we have received it and then we try to read the Body returned in our request     defer res.Body.Close()     body, _ := ioutil.ReadAll(res.Body) Here's complete function accepting URL & Application Key as string. Notice I am starting the function name with capital which denotes that I am making this as an exported function. See Exported/Unexported Identifiers In Go for more     func GetTwxServerEntities(myurl string, appkey string) {     url := myurl     req, _ := http.NewRequest("GET", url, nil)     req.Header.Add("appKey", appkey)     req.Header.Add("Accept", "application/json")     req.Header.Add("Cache-Control", "no-cache")     res, err := http.DefaultClient.Do(req)     if err != nil {         log.Println("Failed to get all entity list from the server", err)     }     defer res.Body.Close()     body, _ := ioutil.ReadAll(res.Body)     //fmt.Println(res)     fmt.Println(string(body)) }   Building PUT Request   To send property updates to the ThingWorx Server I'll create NewReader to read the strings which is JSON in this example payload := strings.NewReader("{\"Prop1\" : \"Demo 101\",\"Prop2\" : 1001}") Like GET request NewRequest is invoked to perform the PUT request like so   req, _ := http.NewRequest("PUT", url, payload) Adding the header details : req.Header.Add("appKey", appkey) req.Header.Add("Content-Type", "application/json") req.Header.Add("Cache-Control", "no-cache") Invoke the client to perform the request res, err := http.DefaultClient.Do(req) if err != nil {         log.Println("Failed to Put the value to the ThingWorx server", err)     } Here's the complete function which takes a URL and appKey and then updates 2 property values for a Thing on the ThingWorx Server:   e.g. myurl= http://tw831psql:8080/Thingworx/Things/RESTThing/Properties/*   func TwxPut(myurl string, appkey string) {     url := myurl     payload := strings.NewReader("{\"Prop1\" : \"Demo 101\",\"Prop2\" : 1001}")     req, _ := http.NewRequest("PUT", url, payload)     req.Header.Add("appKey", appkey)     req.Header.Add("Content-Type", "application/json")     req.Header.Add("Cache-Control", "no-cache")     res, err := http.DefaultClient.Do(req)     if err != nil {         log.Println("Failed to Put the value to the ThingWorx server", err)     }     fmt.Println(res)      } And I can now verify that the property has been updated for the Thing called RESTThing   Building POST Request   Similar to GET & PUT we have to create new Request of method POST to invoke a Service in this example, for this I have already created a service that counts up a numeric property value stored in the CountUpProp property already existing under the RESTThing entity   req, _ := http.NewRequest("POST", url, nil) Adding the Headers to the req req.Header.Add("appKey", appKey) req.Header.Add("Content-Type", "application/json") req.Header.Add("Cache-Control", "no-cache") Handling response and the error in case of an issue res, err := http.DefaultClient.Do(req)     if err != nil {         log.Println("Posting to Thingworx server failed with error", err)     }     fmt.Println(res) Here's complete thought : func TwxPost(myurl string, appKey string) {     // e.g. http://tw831psql:8080/Thingworx/Things/RESTThing/Services/CountUpService     url := myurl     req, _ := http.NewRequest("POST", url, nil)     req.Header.Add("appKey", appKey)     req.Header.Add("Content-Type", "application/json")     req.Header.Add("Cache-Control", "no-cache")     res, err := http.DefaultClient.Do(req)     if err != nil {         log.Println("Posting to Thingworx server failed with error", err)     }     fmt.Println(res) } Verifying property update after the service invoke   All the above functions now can be called for e.g. in a main()   func main() {     var myurl string     var appkey string     // Provide URL for ThingWorx fmt.Println("Enter URL, eg. http://localhost:8080/Thingworx/Server") // accepting URL at runtime     fmt.Scanln(&myurl)     // Provide appKey from the ThingWorx platform fmt.Println("Enter valid ThingWorx Application Key ") // accepting appKey at runtime     fmt.Scanln(&appkey)     GetTwxServerEntities(myurl, appkey)     TwxPut(myurl, appkey)     TwxPost(myurl, appkey) }  
View full tip
Data Model Implementation Guide Part 1   Overview   This project will introduce you to methods for creating the data model that you have designed and are ready to implement. Following the steps in this guide, you will implement the Data Model you've already designed. After having insight into your desired Data Model, this guide will provide instructions and examples on how to build out your application using the ThingWorx platform. We will teach you how to utilize the ThingWorx platform to implement your fully functional IoT application. NOTE: This guide’s content aligns with ThingWorx 9.3. The estimated time to complete ALL 3 parts of this guide is 60 minutes. All content is relevant but there are additional tools and design patterns you should be aware. Please go to this link for more details.     Step 1: Completed Example   Download the completed files for this tutorial:  DataModelEntities.xml. The DataModelEntities.xml file provided to you contains a completed example of the completed data model implementation. Utilize this file to see a finished example and return to it as a reference if you become stuck during this guide and need some extra help or clarification. Keep in mind, this download uses the exact names for entities used in this tutorial. If you would like to import this example and also create entities on your own, change the names of the entities you create.   Step 2: Data Model Scenario   This guide will implement the scenario shown in the Data Model Design guide. Let's revisit our Smart Factory example scenario. Name Description Operations User to keep the line running and make sure that it’s producing quality products Maintenance User to keep machines up and running so that the operator can crank out products Management User in charge of dispatching production orders and making sure the quotas are being met Conveyor Belts Thing on factory line to pass items along to the next stage Pneumatic Gate Thing on factory line Robotic Arm Thing on factory line Quality Check Camera Final Thing on factory line to ensure quality In order to add this to our solution, we will want to build a "connector" between ThingWorx and the existing system. These connectors will be Things as well. Internal system connection Thing for Production Order System Internal system connection Thing for Maintenance Request System Operator   Required Functionality Description 1 File Maintenance Request 2 Get quality data from assets on their line 3 Get performance data for the whole line 4 Get a prioritized list of production orders for their line 5 Create Maintenance Requests   Required Information Description 1 Individual asset performance metrics 2 Full line performance metrics 3 Product quality readings   Maintenance   Required Functionality Description 1 Get granular data values from all assets 2 Get a list of maintenance requests 3 Update maintenance requests 4 Set triggers for automatic maintenance request generation 5 Automatically create maintenance requests when triggers have been activated   Required Information Description 1 Granular details for each asset: In order to better understand healthy asset behavior 2 Current alert status for each asset: to know if there is something going wrong with an asset 3 When the last maintenance was performed on an asset 4 When the next maintenance is scheduled for an asset 5 Maintenance request info: Creation date, due date, progress notes   Management   Required Functionality Description 1 Create production orders 2 Update production orders 3 Cancel Production orders 4 Access line productivity data 5 Elevate maintenance request priority   Required Information Description 1 Production line productivity levels (OEE) 2 List of open Maintenance requests   Overlapping Matrix   This matrix represents all of the overlapping Components that are shared by multiple types of Things in our system:   Unique Matrix   This matrix represents the unique Components to each type of Thing:     Step 3: LineAsset Thing Template   After prioritizing and grouping common functionality and information, we came up with the list below for the first Thing Template to create, LineAsset with five Properties, one Event, and one Subscription. The breakdown for the LineAsset Thing Template is as follows:   Follow the below instruction to create this Entity and get the implementation phase of your development cycle going.   Line Asset Properties   Let's build out our Properties. In the ThingWorx Composer, click the + New at the top of the screen. Select Thing Template in the dropdown.        3. In the name field, enter LineAsset and set the Project (ie, PTCDefaultProject). 4. For the Base Thing Template field, select GenericThing.     5. Click Save.  6. Switch to the Properties and Alerts tab.  7. Click the plus button to add a new Property.   The Properties for the LineAsset Thing Template are as follows: Name Base Type Aspects Data Change Type State String Persistent and Logged ALWAYS SerialNumber String Persistent, Read Only, and Logged NEVER LastMaintenance DATETIME Persistent and Logged VALUE NextMaintenance DATETIME Persistent and Logged VALUE PowerConsumption NUMBER, Min Value: 0 Persistent and Logged ALWAYS Follow the next steps for all the properties shown in our template property table. Click Add. Enter the name of the property (ie, State). Select the Base Type of the proprty from the dropdown. Check the checkboxes for the property Aspects. Select the Data Change Type from the dropdown.   Click Done when finished creating the property. Your properties should match the below configurations.     Line Asset Event   Switch to the Events tab. Click Add. Enter the name of the Event (ie, Error). Select AlertStatus as the Data Shape. This DataShape will allow us to provide simple information including an alert type, the property name, and a status message.   Click Done. Your Event should match the below configurations.          Line Asset Subscription   Switch to the Subscriptions tab. Click Add. Check the Enabled checkbox. Switch to the Inputs tab. Select the name of the Event (ie, Error). Click Done. Your Subscription should match the below configurations.             Challenge Yourself   We have left the Subscription code empty. Think of a way to handle Error Events coming from your line asset and implement it in this section.   Click here to view Part 2 of this guide. 
View full tip
Announcements