From 77bf5cd59e6c3bdd41f0c325f59f685f86bace5e Mon Sep 17 00:00:00 2001 From: selsner-ptc <71039601+selsner-ptc@users.noreply.github.com> Date: Wed, 20 Mar 2024 16:27:39 -0400 Subject: [PATCH 1/5] Initial commit of very simple TCP client --- .../Very Simple TCP Client/README.md | 5 + .../very_simple_tcp_client.js | 173 ++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 Generic Use Cases/Very Simple TCP Client/README.md create mode 100644 Generic Use Cases/Very Simple TCP Client/very_simple_tcp_client.js 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; +} From bd619394ac37b6c93508c965f23a02b866a184e6 Mon Sep 17 00:00:00 2001 From: selsner-ptc <71039601+selsner-ptc@users.noreply.github.com> Date: Wed, 20 Mar 2024 16:28:54 -0400 Subject: [PATCH 2/5] Initial commit of light stream processor --- ...UDD Lightweight Stream Processor 202310.js | 234 ++++++++++++++++++ ...TG Lightweight Stream Processor 202310.opf | Bin 0 -> 175534 bytes .../Light Stream Processing/README.md | 26 ++ 3 files changed, 260 insertions(+) create mode 100644 Generic Use Cases/Light Stream Processing/JS - UDD Lightweight Stream Processor 202310.js create mode 100644 Generic Use Cases/Light Stream Processing/OPF - UDD IOTG Lightweight Stream Processor 202310.opf create mode 100644 Generic Use Cases/Light Stream Processing/README.md 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 0000000000000000000000000000000000000000..ed491f48c97003f42dea504592c7598a2569d097 GIT binary patch literal 175534 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

_O|` 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 h;bb{W4c4*k1-=imLMfA?$u?myt&{{}*ca%lhn literal 0 HcmV?d00001 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 From 129a17727329dfc30bd5681fbf32c8140aa9101a Mon Sep 17 00:00:00 2001 From: selsner-ptc <71039601+selsner-ptc@users.noreply.github.com> Date: Tue, 3 Jun 2025 16:28:43 -0400 Subject: [PATCH 3/5] Create base64 encode and decode.js --- Helper Functions/base64 encode and decode.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Helper Functions/base64 encode and decode.js diff --git a/Helper Functions/base64 encode and decode.js b/Helper Functions/base64 encode and decode.js new file mode 100644 index 0000000..e69de29 From 79203b20e3a422235df823f1365939e07a68ea3e Mon Sep 17 00:00:00 2001 From: selsner-ptc <71039601+selsner-ptc@users.noreply.github.com> Date: Tue, 3 Jun 2025 16:46:24 -0400 Subject: [PATCH 4/5] Delete base64 encode and decode.js --- Helper Functions/base64 encode and decode.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Helper Functions/base64 encode and decode.js diff --git a/Helper Functions/base64 encode and decode.js b/Helper Functions/base64 encode and decode.js deleted file mode 100644 index e69de29..0000000 From 9f99d1700b5c73655d50ca769e11e0ca4fecd7f5 Mon Sep 17 00:00:00 2001 From: selsner-ptc <71039601+selsner-ptc@users.noreply.github.com> Date: Tue, 3 Jun 2025 17:02:34 -0400 Subject: [PATCH 5/5] Create base64 encode and decode.js --- Helper Functions/base64 encode and decode.js | 69 ++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 Helper Functions/base64 encode and decode.js 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