diff --git a/Generic Use Cases/Light Stream Processing/JS - UDD Lightweight Stream Processor 202310.js b/Generic Use Cases/Light Stream Processing/JS - UDD Lightweight Stream Processor 202310.js new file mode 100644 index 0000000..bf2246f --- /dev/null +++ b/Generic Use Cases/Light Stream Processing/JS - UDD Lightweight Stream Processor 202310.js @@ -0,0 +1,234 @@ +/***************************************************************************** + * + * This file is copyright (c) 2023 PTC Inc. + * All rights reserved. + * + * Name: Lightweight Extract, Load and Transform Stream Processor using + * Kepware Universal Device Driver and IoT Gateway Plugin + * + * Description: This example profile receives, prepares and processes continuous + * incoming data and caches the results for access and distribution across all + * Kepware publisher and server interfaces. + * + * Data input is expected from a local Kepware IoT Gateway (IOTG) REST Client Agent + * configured to publish on Interval in Wide Format using default Standard Message + * Template. Tags can be added or removed from the REST Agent via GUI or API to include + * or exclude from UDD stream processing. + * + * Fewer bytes - e.g. fewer keys and shorter paths - allows more tags per UDD processor. + * + * Version: 1.0.0 + * + * Notes: To create one complete processor one IOTG REST Client Agent should publish + * to one UDD channel/device. This single profile can be shared across all UDD channels. + * + * Benchmark: Light testing of this simple profile showed a maximum throughput + * and stable performance of ~100,000 byte message sizes processed at <500 ms intervals. + * This allows ~1200 tags with ~32 char full path names & default IOTG REST Client + * JSON format. + * -- Time measured from observation of IOTG HTTP POST to observation of UDD HTTP ACK + * -- Specs used for testing: Kepware Server 6.14.263 - Windows 11 - i7-12800H + * +****************************************************************************** + + +/** OPTIONAL DEVELOPER GLOBALS AND FUNCTIONS */ + +// Global variable for driver version +const VERSION = "2.0"; + +// Global variable for driver mode +const MODE = "Server" + +// Status types +const ACTIONRECEIVE = "Receive" +const ACTIONCOMPLETE = "Complete" +const ACTIONFAILURE = "Fail" +const READ = "Read" +const WRITE = "Write" + +// Add buffer to handle fragmentation of messages above typical MTU/1500 bytes +var BUFFER = '' + +// Helper function to translate string to bytes +function stringToBytes(str) { + let byteArray = []; + for (let i = 0; i < str.length; i++) { + let char = str.charCodeAt(i) & 0xFF; + byteArray.push(char); + } + return byteArray; +} + +/* REQUIRED DRIVER FUNCTIONS (onProfileLoad, onValidateTag, onTagsRequest, onData) */ + +/** + * onProfileLoad() - Allow the server to retrieve driver profile metadata + * + * @return {OnProfileLoadResult} - Driver metadata + */ + + function onProfileLoad() { + + // Initialize driver cache + try { + initializeCache(); + + } catch (e){ + log('Error from initializeCache() during onProfileLoad(): ' + e.message) + } + + return { version: VERSION, mode: MODE }; +} + +/** + * onValidateTag(info) - Allow the server to validate a tag address + * + * @param {object} info - Object containing the function arguments. + * @param {Tag} info.tag - Single tag. + * + * @return {OnValidateTagResult} - Single tag with a populated '.valid' field set. +*/ + +function onValidateTag(info) { + + /** + * Define Regular Expression rules + * + * @param {string} regex - a regex string + */ + + // This example supports any address syntax + let regex = /^(.*?)/; + + // Test tag address against regex and if valid cache address and initial value + try { + // Validate the address against the regular expression + if (regex.test(info.tag.address)) { + info.tag.valid = true; + // This example assigns a default data types of string + if (info.tag.dataType === 'Default'){ + info.tag.dataType = 'String' + } + log('onValidateTag - address "' + info.tag.address + '" is valid') + return info.tag + } + else { + info.tag.valid = false; + log("ERROR: Tag address '" + info.tag.address + "' is not valid"); + } + + return info.tag.valid; + } + + catch(e) { + // Use log to provide helpful information that can assist with error resolution + log("ERROR: onValidateTag - Unexpected error: " + e.message); + info.tag.valid = false; + return info.tag; + } +} + +/** + * onTagsRequest(info) - Handle server requests for tags to be read and written + * + * @param {object} info - Object containing the function arguments. + * @param {MessageType} info.type - Communication mode for tags. Can be undefined. + * @param {Tag[]} info.tags - Tags currently being processed. Can be undefined. + * + * @return {OnTransactionResult} - The action to take, tags to complete (if any) and/or data to send (if any). + */ + + function onTagsRequest(info) { + switch(info.type){ + case READ: + // If first read of value then intialize cache with appropriate default value + if (readFromCache(info.tags[0].address).value !== undefined){ + info.tags[0].value = readFromCache(info.tags[0].address).value + return { action: ACTIONCOMPLETE, tags: info.tags } + } + else { + writeToCache(info.tags[0].address, '') + info.tags[0].value = '' + return { action: ACTIONCOMPLETE, tags: info.tags } + } + + case WRITE: + // Writes are not built into this example + log(`ERROR: onTagRequest - Write command for address "${info.tags[0].address}" is not supported.`) + return { action: ACTIONFAILURE }; + default: + log(`ERROR: onTagRequest - Unexpected error. Command type unknown: ${info.type}`); + return { action: ACTIONFAILURE }; + } +} + +/** + * onData(info) - Process raw driver data + * + * @param {object} info - Object containing the function arguments. + * @param {MessageType} info.type - Communication mode for tags. Can be undefined. + * @param {Tag[]} info.tags - [Not used in this example] Tags currently being processed. Can be undefined. + * @param {Data} info.data - Incoming set of "raw" bytes; parse and assign other data types as needed + * + * @return {OnTransactionResult} - The action to take, tags to complete (if any) and/or data to send (if any). + */ + + function onData(info) { + + /* + PREPARATION: This first section prepares the received data for processing + */ + + // This example expects to receive plain text messages so the entire set of bytes is assigned string + let stringData = ""; + for (let i = 0; i < info.data.length; i++) { + stringData += String.fromCharCode(info.data[i]); + } + + // Append received data to buffer + BUFFER+=stringData + + // Create object to hold parsed JSON object of tag names and values from Kepware IOTG + var jsonData + + // Parse JSON structure from buffer and assign to object + try { + let jsonStr = BUFFER.substring(BUFFER.indexOf('{'), BUFFER.lastIndexOf('}]}') + 3 ); + jsonData = JSON.parse(jsonStr) + // Clear buffer if parsing succeeds + BUFFER = '' + } + catch(e) { + // If parsing fails wait for more and try again + return { action: ACTIONRECEIVE } + } + + /* + PROCESSING: This next section processes the prepared data and saves results to cache + */ + + // If a tag value is True add tag name to comma-seperated list + try { + const tagList = [] + if (jsonData.values) { + jsonData.values.forEach(({ id, v }) => { + if (v !== false) { + tagList.push(id) + } + }) + let tagStringNames = tagList.toString() + writeToCache('result:true_tags', tagStringNames) + } + // Send HTTP ACK to related HTTP Publisher in Kepware IOTG after data is processed + let httpAck = 'HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nKeep-Alive: timeout=10\r\nContent-Length: 0\r\n\r\n' + const httpABytes = stringToBytes(httpAck) + return { action: ACTIONCOMPLETE, data: httpABytes } + } + catch(e) { + log(`ERROR: ${e}`); + let httpNAck = 'HTTP/1.1 400 BAD REQUEST\r\n' + let httpNBytes = stringToBytes(httpNAck) + return { action: ACTIONFAILURE, data: httpNBytes } + } +} \ No newline at end of file diff --git a/Generic Use Cases/Light Stream Processing/OPF - UDD IOTG Lightweight Stream Processor 202310.opf b/Generic Use Cases/Light Stream Processing/OPF - UDD IOTG Lightweight Stream Processor 202310.opf new file mode 100644 index 0000000..ed491f4 Binary files /dev/null and b/Generic Use Cases/Light Stream Processing/OPF - UDD IOTG Lightweight Stream Processor 202310.opf differ diff --git a/Generic Use Cases/Light Stream Processing/README.md b/Generic Use Cases/Light Stream Processing/README.md new file mode 100644 index 0000000..4e14811 --- /dev/null +++ b/Generic Use Cases/Light Stream Processing/README.md @@ -0,0 +1,26 @@ +# Lightweight Stream Processors using Kepware Universal Device Driver and IoT Gateway REST Client Agent + +## Quick Start: + +1. Load Kepware project file + - Includes simulation tags, Javascript for Universal Device Driver and one "stream processor"- a single IoT Gateway REST Client Agent publishing to a single UDD channel/device +2. Launch Quick Client from the Kepware Configuration tool + - Observe initial value of udd1.processor.result.true_tags + - Observe initial values of 600 simulated Boolean tags + - Adjust Boolean state of one or more simulated tags to True (write a non-zero value to the tags using Quick Client) + - Observe names of tags with True states organized a comma-seperated string in udd1.processor.result.true_tags + +## Overview + +This example demonstrates how Kepware Universal Device Driver can be used with IoT Gateway Client Agents to act together as lightweight stream processors. The UDD profile receives, prepares and processes continuous incoming data and caches the results for access and distribution across all Kepware publisher and server interfaces. Data input is published from a local Kepware IoT Gateway (IOTG) REST Client Agent. Tags from any Kepware driver can be added or removed from the REST Client Agent via GUI or API to be included or excluded from UDD stream processing. + +The simple processor included in the example currently observes Boolean tags for True states and provides a comma-seperated list of tag names: + +"simulator.data.600_bools.bool107,simulator.data.600_bools.bool5,simulator.data.600_bools.bool599" + +Notes: To create one complete processor, one IOTG REST Client Agent should publish to one UDD channel/device. This profile can be shared across all UDD channels. + +Benchmarks: Light testing for maximum throughput and stable operation of this profile showed maximum ~500 ms processing intervals with a message size of ~100,000 bytes. This yields around 1200 tags with ~32 char full path names & REST Client Agent publishing in Wide Format with Standard Message Template. +-- Time measured from observation of REST Client Agent HTTP POST to observation of UDD HTTP ACK +-- REST Client Agent configured to publish on Interval at 10 ms in Wide Format using default Standard Message Template +-- Specs used for testing: Kepware Server 6.14.263 - Windows 11 - i7-12800H \ No newline at end of file diff --git a/Generic Use Cases/Very Simple TCP Client/README.md b/Generic Use Cases/Very Simple TCP Client/README.md new file mode 100644 index 0000000..b115448 --- /dev/null +++ b/Generic Use Cases/Very Simple TCP Client/README.md @@ -0,0 +1,5 @@ +# Very Simple TCP Client + +## Overview + +This example demonstrates a basic TCP client driver that sends requests, processes responses and updates tags. \ No newline at end of file diff --git a/Generic Use Cases/Very Simple TCP Client/very_simple_tcp_client.js b/Generic Use Cases/Very Simple TCP Client/very_simple_tcp_client.js new file mode 100644 index 0000000..76655af --- /dev/null +++ b/Generic Use Cases/Very Simple TCP Client/very_simple_tcp_client.js @@ -0,0 +1,173 @@ +/***************************************************************************** + * + * This file is copyright (c) PTC, Inc. + * All rights reserved. + * + * Name: Very Simple TCP ASCII Driver Example + * Description: A simple example profile that demonstrates how to send a + * request, handle the response and share data with the server + * + * Developed on Kepware Server version 6.15, UDD V2.0 + * + * Version: 0.1.1 +******************************************************************************/ + +/** Global variable for UDD API version */ +const VERSION = "2.0"; + +/** Global variable for driver mode */ +const MODE = "Client" + +/* Global variables for action */ +const ACTIONCOMPLETE = "Complete" +const ACTIONFAILURE = "Fail" +const ACTIONRECEIVE = "Receive" + +// Global variable for all Kepware supported data_types +const data_types = { + DEFAULT: "Default", + STRING: "String", + BOOLEAN: "Boolean", + CHAR: "Char", + BYTE: "Byte", + SHORT: "Short", + WORD: "Word", + LONG: "Long", + DWORD: "DWord", + FLOAT: "Float", + DOUBLE: "Double", + BCD: "BCD", + LBCD: "LBCD", + LLONG: "LLong", + QWORD: "QWord" +} + +/** + * Retrieve driver metadata. + * + * @return {OnProfileLoadResult} - Driver metadata. + */ + + + function onProfileLoad() { + + // Initialized our internal cache + try { + initializeCache(); + + } catch (e){ + // If this fails it means the cache has already been initialized + } + + return { version: VERSION, mode: MODE }; +} + +/** + * Validate an address. + * + * @param {object} info - Object containing the function arguments. + * @param {Tag} info.tag - Single tag. + * + * @return {OnValidateTagResult} - Single tag with a populated '.valid' field set. + * + * + * +*/ +function onValidateTag(info) { + + /** + * The regular expression to compare address to. This example only validates that + * the address is at least one character - letter or number + */ + + let regex = /^\w+$/; + + try { + // Validate the address against the regular expression + if (regex.test(info.tag.address)) { + info.tag.valid = true; + // Fix the data type to the correct one + if (info.tag.dataType === data_types.DEFAULT){ + info.tag.dataType = data_types.STRING + } + log('onValidateTag - address "' + info.tag.address + '" is valid.'); + } + else { + info.tag.valid = false; + log("ERROR: Tag address '" + info.tag.address + "' is not valid"); + } + + return info.tag + } + catch(e) { + // Use log to provide helpful information that can assist with error resolution + log("ERROR: onValidateTag - Unexpected error: " + e.message); + info.tag.valid = false; + return info.tag; + } + +} + +/** + * Handle request for a tag to be completed. + * + * @param {object} info - Object containing the function arguments. + * @param {MessageType} info.type - Communication mode for tags. Can be undefined. + * @param {Tag[]} info.tags - Tags currently being processed. Can be undefined. + * + * @return {OnTransactionResult} - The action to take, tags to complete (if any) and/or data to send (if any). + */ + + function onTagsRequest(info) { + log(`onTagsRequest - info: ${JSON.stringify(info)}`) + + let request = "YOUR REQUEST MESSAGE HERE\n" + // let request = "Hello Server!\n"; + let readFrame = stringToBytes(request); + return { action: ACTIONRECEIVE, data: readFrame }; +} + +/** + * Handle incoming data. + * + * @param {object} info - Object containing the function arguments. + * @param {MessageType} info.type - Communication mode for tags. Can be undefined. + * @param {Tag[]} info.tags - Tags currently being processed. Can be undefined. + * @param {Data} info.data - The incoming data. + * + * @return {OnTransactionResult} - The action to take, tags to complete (if any) and/or data to send (if any). + */ + function onData(info) { + log(`onData - info.tags: ${JSON.stringify(info.tags)}`) + + // Convert the response to a string + let stringResponse = ""; + for (let i = 0; i < info.data.length; i++) { + stringResponse += String.fromCharCode(info.data[i]); + } + + log(`onData - String Response: ${stringResponse}`) + + info.tags[0].value = stringResponse + + return {action: ACTIONCOMPLETE, tags: info.tags} + +} + +/** + * Helper function to translate string to bytes. + * Required. + * + * @param {string} str + * @return {Data} + */ + function stringToBytes(str) { + let byteArray = []; + for (let i = 0; i < str.length; i++) { + let char = str.charCodeAt(i) & 0xFF; + byteArray.push(char); + } + + // return an array of bytes + return byteArray; +} diff --git a/Helper Functions/base64 encode and decode.js b/Helper Functions/base64 encode and decode.js new file mode 100644 index 0000000..d8c62ce --- /dev/null +++ b/Helper Functions/base64 encode and decode.js @@ -0,0 +1,69 @@ +/***************************************************************************** + * + * This file is copyright (c) PTC, Inc. + * All rights reserved. + * + * Name: base64 encode and decode.js + * NOTE: Made with LLM + source at https://mathiasbynens/base64 + * Example uses: + * -- Encode: base64(, encode = true) + * -- Decode: base64(, encode = false) + * + * Update History: + * 0.0.1: Initial Release + * + * Version: 0.0.1 +******************************************************************************/ + +function base64(input, encode = true) { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + + const InvalidCharacterError = function (message) { + this.message = message; + }; + InvalidCharacterError.prototype = new Error(); + InvalidCharacterError.prototype.name = 'InvalidCharacterError'; + + const btoa = function (input) { + let str = String(input); + let output = ''; + for ( + let block, charCode, idx = 0, map = chars; + str.charAt(idx | 0) || (map = '=', idx % 1); + output += map.charAt(63 & (block >> (8 - (idx % 1) * 8))) + ) { + charCode = str.charCodeAt((idx += 3 / 4)); + if (charCode > 0xff) { + throw new InvalidCharacterError( + "'btoa' failed: The string to be encoded contains characters outside of the Latin1 range." + ); + } + block = (block << 8) | charCode; + } + return output; + }; + + const atob = function (input) { + let str = String(input).replace(/=+$/, ''); + if (str.length % 4 == 1) { + throw new InvalidCharacterError( + "'atob' failed: The string to be decoded is not correctly encoded." + ); + } + let output = ''; + for ( + let bc = 0, bs, buffer, idx = 0; + (buffer = str.charAt(idx++)); + ~buffer && + ((bs = bc % 4 ? bs * 64 + buffer : buffer), + bc++ % 4) + ? (output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6)))) + : 0 + ) { + buffer = chars.indexOf(buffer); + } + return output; + }; + + return encode ? btoa(input) : atob(input); +} \ No newline at end of file