/******************************************************************************* * Copyright (c) 2013 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html ******************************************************************************/ /** * A javascript library for accessing live process data in web browser using * WebSocket. * * @version 1.0.0 * * @author Xihui Chen * */ /** * Global debug flag. */ var WebPDA_Debug = false; /** * Utility object to provide general utility functions. * @namespace */ var WebPDA_Util = {}; /** * Create a new WebPDA object, which establish a new connection to the server. * @class WebPDA * @constructor * @param url url of the webpda server. * @param username user name * @param password password for the user * @returns a new WebPDA object. */ function WebPDA(url, username, password) { var pvID = 0; var internalPVID = 0; var pvArray = []; var internalPVArray = []; var websocket = null; var webpdaSelf = this; var webSocketOnOpenCallbacks = []; var webSocketOnCloseCallbacks = []; var webSocketOnErrorCallbacks = []; var onServerMessageCallbacks = []; this.isLive = false; openWebSocket(url); /**A callback function on WebSocket open/close/error event. * @callback WebPDA~WebSocketEventCallback * @param {WebSocket.Event} event the WebSocket event. */ /** * Add a callback to WebSocket onOpen event. * @param {WebPDA~WebSocketEventCallback} callback the callback function on WebSocket open event. */ this.addWebSocketOnOpenCallback = function(callback) { webSocketOnOpenCallbacks.push(callback); }; /** * Remove a WebSocket onOpen callback. * @param {WebPDA~WebSocketEventCallback} callback the callback function on WebSocket open event. */ this.removeWebSocketOnOpenCallback = function(callback){ webSocketOnOpenCallbacks.splice(webSocketOnOpenCallbacks.indexOf(callback), 1); }; /** * Add a callback to WebSocket onClose event. * @param {WebPDA~WebSocketEventCallback} callback the callback function on WebSocket close event. * */ this.addWebSocketOnCloseCallback = function(callback) { webSocketOnCloseCallbacks.push(callback); }; /** * Add a callback to WebSocket onError event. * @param {WebPDA~WebSocketEventCallback} callback the callback function on WebSocket error event. * */ this.addWebSocketOnErrorCallback = function(callback) { webSocketOnErrorCallbacks.push(callback); }; /**A callback function that will be notified when there is a message from server. * @callback WebPDA~OnServerMessageCallback * @param {object} message the message object which is usually an error or info message in following format: * {"msg":"Info","title":"the title","details":"The details"} * * * */ /** * Add a callback that will be notified when there is a notification message from server. * @param {WebPDA~OnServerMessageCallback} callback the callback * */ this.addOnServerMessageCallback = function(callback) { onServerMessageCallbacks.push(callback); }; /** * Set PV's value by the PV's id. * @param {number} id id of the PV * @param {object} value to be written. Type of the value should be acceptable by the PV. */ this.setPVValueById = function(id, value) { if (pvArray[id] != null) { this.setPVValue(pvArray[id], value); } }; /** * Set PV Value. * * @param {WebPDA~PV} pv * the PV * @param {object} value * the value to be set. It must be a value type that the PV can accept, * for example, a number for numeric PV. */ this.setPVValue = function(pv, value) { var json = JSON.stringify({ "commandName" : "SetPVValue", "id" : pv.internalPV.id, "value" : value }); this.sendText(json); }; /** * Set server side buffer size. The server side buffer * is used to temporarily buffer the data to be sent to client when there is temporary disconnection, * so the client won't lose any data for temporary disconnection. The connection will be * closed by server when the buffer is full. * The default buffer size is 100K. The max allowed size is 1M. * * @param {number} size * buffer size in byte.If the buffer size is larger than 1M, it will be coerced * to 1M. */ this.setServerBufferSize = function(size) { var json = JSON.stringify({ "commandName" : "SetServerBufferSize", "size" : size }); this.sendText(json); }; /** * Close Websocket. */ this.close = function() { if (websocket != null) websocket.close(); websocket = null; }; /** * Send login command to server. * @param {string} username * @param {string} password */ this.login = function(username, password){ var json = JSON.stringify({ "commandName" : "Login", "username": username, "password": password }); this.sendText(json); }; /** * Send logout command to server. */ this.logout = function() { var json = JSON.stringify({ "commandName" : "Logout" }); this.sendText(json); }; /**Get all PVs on this client. * @returns {Array.<WebPDA~PV>} All PVs in an array. */ this.getAllPVs = function() { return pvArray; }; /** * Get the PV from its id. * @param {number} id id of the PV. * @returns {WebPDA~PV} the PV. */ this.getPV = function(id){ return pvArray[id]; }; /** * Send text to server using WebSocket. * This function is for internal use only. * @param {string} text */ this.sendText =function(text) { if (WebPDA_Debug) console.log("sending " + text); websocket.send(text); }; /** * Create PV internally. This function should only be called by * subclass. Client should not call this function. * @param {object} parameterObj the object that contains parameters to create the PV, which * can be used to identify this internal PV. * @param {function} comapareFunc the function to compare if two internal PVs are identical to * avoid creating an extra channel to server. */ this.internalCreatePV = function(name, parameterObj, compareFunc, bufferAllValues) { var internalPV = getInternalPV(parameterObj, compareFunc); if (internalPV == null) { internalPV = new WebPDAInternalPV(internalPVID, this); internalPV.parameterObj = parameterObj; internalPVArray[internalPVID] = internalPV; var createPVCmd = { commandName : "CreatePV", id : internalPVID }; var json = JSON.stringify(WebPDA_Util.extend(createPVCmd, parameterObj)); if(this.isLive) this.sendText(json); else{ var webpdaSelf = this; var listener = null; listener = function(evt){ webpdaSelf.sendText(json); setTimeout(function(){ webpdaSelf.removeWebSocketOnOpenCallback(listener); }, 0); }; this.addWebSocketOnOpenCallback(listener); } internalPVID++; } var pv = new PV(name); pv.internalPV = internalPV; internalPV.addPV(pv); pv.bufferAllValues = bufferAllValues; pvArray[pvID] = pv; pv.id = pvID; pvID++; return pv; }; function fireOnOpen(evt) { webpdaSelf.isLive = true; for ( var i in webSocketOnOpenCallbacks) { webSocketOnOpenCallbacks[i](evt); } } function fireOnClose(evt) { for(var i in internalPVArray){ internalPVArray[i].firePVEventFunc({ pv:internalPVArray[i].id, "e": "conn", "d": false}); } for ( var i in webSocketOnCloseCallbacks) { webSocketOnCloseCallbacks[i](evt); } } function fireOnError(evt) { for ( var i in webSocketOnErrorCallbacks) { webSocketOnErrorCallbacks[i](evt); } } function fireOnServerMessage(json) { for ( var i in onServerMessageCallbacks) { onServerMessageCallbacks[i](json); } } /** * Get internal pv from registered pvs. * * @param parameterObj * the object that contains parameters to create the pv. * @param compareFunc * the compare function to determine if two PVs are considered * the same pv. * @returns the internal pv. * @ignore */ function getInternalPV(parameterObj, compareFunc) { for ( var i in internalPVArray) { if (internalPVArray[i] != null && internalPVArray[i] != undefined) { if (compareFunc(parameterObj, internalPVArray[i].parameterObj)) return internalPVArray[i]; } } return null; } function closeInternalPV(internalPVId) { var json = JSON.stringify({ "commandName" : "ClosePV", "id" : internalPVId }); webpdaSelf.sendText(json); delete internalPVArray[internalPVId]; } function pauseInternalPV(internalPVId, paused){ var json = JSON.stringify({ "commandName" : "PausePV", "id" : internalPVId, "paused": paused }); webpdaSelf.sendText(json); } function openWebSocket(url) { if (websocket != null) throw new Error( "Please close current websocket before opening a new one."); if ('WebSocket' in window) { websocket = new WebSocket(url, "org.webpda"); } else if ('MozWebSocket' in window) { websocket = new MozWebSocket(url, "org.webpda"); } else { throw new Error('WebSocket is not supported by this browser.'); } websocket.binaryType = "arraybuffer"; websocket.onopen = function(evt) { webpdaSelf.login(username, password); fireOnOpen(evt); }; websocket.onmessage = function(evt) { var json; if(typeof evt.data == "string"){ json = JSON.parse(evt.data); if (WebPDA_Debug) console.log("received: " + evt.data); }else{ json = preprocessBytesArray(evt.data); if (WebPDA_Debug) console.log("received: " + evt.data + " "+evt.data.byteLength); } dispatchMessage(json); }; websocket.onclose = function(evt) { this.isLive =false; if (WebPDA_Debug) console.log("websocket closed:" + url); for ( var i in pvArray) { pvArray[i].firePVEventFunc({ pv : i, e : "conn", d : false }); } fireOnClose(evt); }; websocket.onerror = function(evt) { fireOnError(evt); }; } function preprocessBytesArray(data){ var json= new Object(); var int32Array = new Int32Array(data); if(int32Array[0]==0){ json.e="val"; }else if(int32Array[0]==1) json.e="bufVal"; json.pv = int32Array[1]; json.d = data; return json; } function dispatchMessage(json) { if (json.msg != null) handleServerMessage(json); if (json.pv != null) { if (internalPVArray[json.pv] != null) internalPVArray[json.pv].firePVEventFunc(json); } } function handleServerMessage(json) { if(json.msg == "Ping"){ var pong = JSON.stringify({ "commandName" : "Pong", "count": json.Count }); webpdaSelf.sendText(pong); }else if(json.msg == "Error"){ console.log("Error: " + json.title + " - " + json.details); }else if(WebPDA_Debug) console.log(json); fireOnServerMessage(json); } /** * The internal pv that actually maps to the connection to server one on one. * This Object is exposed for inheritance purpose. End user is not supposed to access it. * @ignore */ function WebPDAInternalPV(id, webPDA) { this.myPVs = []; //the WebPDA session this.webPDA=webPDA; this.id = id; // latest value of the pv this.value = null; // if all values are buffered. this.bufferAllValues = false; // all buffered values if bufferAllValues is true this.allBufferedValues = []; this.connected = false; this.writeAllowed = false; this.isPaused = false; // The object that identifies the pv this.parameterObj = null; } //fire a pv event WebPDAInternalPV.prototype.firePVEventFunc = function(json) { // update the internal properties of the pv // processJson should be implemented in specific protocol library this.webPDA.processJsonForPV(this, json); for ( var i in this.myPVs) { this.myPVs[i].firePVEventFunc(json); } // console.log("fire pv event" + json.e + " " + json.d); }; WebPDAInternalPV.prototype.addPV = function(pv) { this.myPVs.push(pv); }; WebPDAInternalPV.prototype.closePV = function(pv) { delete this.webPDA.getAllPVs()[pv.id]; for ( var i in this.myPVs) { if (this.myPVs[i].id == pv.id) { this.myPVs.splice(i, 1); break; } } if(this.myPVs.length>0) return; closeInternalPV(this.id); }; WebPDAInternalPV.prototype.pausePV = function(pv) { var allPVsPaused = true; for ( var i in this.myPVs) { if (!this.myPVs[i].isPaused()) { allPVsPaused = false; break; } } if(allPVsPaused != this.isPaused) pauseInternalPV(this.id, allPVsPaused); this.isPaused = allPVsPaused; }; /** * The Process Variable that is actually exposed to end user. * @constructor * @param name * name of the PV. * @returns the pv object. */ function PV(name) { /**Name of the PV. * @type {string} */ this.name = name || ""; /**Id of the PV. * @type {number} */ this.id = -1; //The InternalPV that actually maps to the connection this.internalPV = null; this.pvCallbacks = []; this.paused = false; } /**If the pv is connected to the device. * @returns {boolean} true if the pv is connected. */ PV.prototype.isConnected = function(){ return this.internalPV.connected; }; /** * If all values are buffered during the update period. If false, only * the latest value is preserved. * @returns {boolean} true if values are buffered. */ PV.prototype.isBufferingAllValues = function(){ return this.internalPV.bufferAllValues; }; /** * If write operation is allowed on the pv * @return {boolean} */ PV.prototype.isWriteAllowed = function(){ return this.internalPV.writeAllowed; }; /** * If the pv is paused * @return {boolean} */ PV.prototype.isPaused = function(){ return this.paused; }; /** * Get value of the PV. * return {object} the value which is a data structure depending on the PV. */ PV.prototype.getValue = function(){ return this.internalPV.value; }; /** * Get all buffered values in an array. * return {Array.<object>} an object array in which each object is a PV value. */ PV.prototype.getAllBufferedValues = function(){ return this.internalPV.allBufferedValues; }; // fire a pv event PV.prototype.firePVEventFunc = function(json) { if (this.paused) return; for ( var i in this.pvCallbacks) { this.pvCallbacks[i](json.e, this, json.d); } }; /**A callback function that is notified on PV's event. * @callback WebPDA~PV~PVCallback * @param {string} event the event on the PV. For a control system PV, * it could be "conn" (connection state changed), "val" (value changed), * "bufVal"(buffered values changed if PV is buffering values), "error", * "writePermission" (write permission changed), "writeFinished". * @param {WebPDA~PV} pv the PV itself. * @param {object} data the data associated with this event, for example, an error message * object for error event. */ /** * Add a callback to the PV that will be notified on PV's event. * @param {WebPDA~PV~PVCallback} callback the callback function. */ PV.prototype.addCallback = function(callback) { this.pvCallbacks.push(callback); }; /** * Remove a callback. * @param {WebPDA~PV~PVCallback} callback the callback function. */ PV.prototype.removeCallback = function(callback) { for ( var i in this.pvCallbacks) { if (this.pvCallbacks[i] == callback) delete this.pvCallbacks[i]; } }; /** * Set pv value. * @param {object} value * the value to be set. It must be a value type that the PV can accept, * for example, a number for numeric PV. */ PV.prototype.setValue = function(value) { setPVValue(this, value); }; /** * Pause/resume notification on this PV. * @param {boolean} True is setting to pause, false is setting to resume. */ PV.prototype.setPaused = function(paused) { this.paused = paused; this.internalPV.pausePV(this); }; /** * Close the pv to dispose all resources related to the pv. */ PV.prototype.close = function() { this.internalPV.closePV(this); }; } (function() { WebPDA_Util = { extend : extend, clone : clone, formatDate : formatDate, sliceArrayBuffer : sliceArrayBuffer, decodeUTF8Array : decodeUTF8Array }; /** * Extend an object with the members of another * * @param {Object} * a The object to be extended * @param {Object} * b The object to add to the first one */ function extend(a, b) { if (!a) { a = {}; } for ( var i in b) { a[i] = b[i]; } return a; } /** * Deep clone an object. */ function clone(obj) { var r = new Object(); for ( var i in obj) { if (typeof (obj[i]) == "object" && obj[i] != null) r[i] = clone(obj[i]); else r[i] = obj[i]; } return r; } /** * Slice an array buffer from start (inclusive) to end (exclusive). * If end<0, it will slice all right part of the array from start. * @returns a new array which has copy of the sliced part. */ function sliceArrayBuffer(buf, start, end){ if(end<0) end = buf.byteLength; var copy = new ArrayBuffer(end-start); var srcView = new Int8Array(buf); var tgtView = new Int8Array(copy); for(var i=start; i<end; i++){ tgtView[i-start] = srcView[i]; } return copy; } function formatDate(d){ function pad(n){return n<10 ? '0'+n : n;} return d.getFullYear()+'-' + pad(d.getMonth()+1)+'-' + pad(d.getDate())+' ' + pad(d.getHours())+':' + pad(d.getMinutes())+':' + pad(d.getSeconds()); } /** * Decode utf8 byte array to javascript string.... * This piece of code is copied from: * http://ciaranj.blogspot.com/2007/11/utf8-characters-encoding-in-javascript.html */ function decodeUTF8Array(dotNetBytes) { var result= ""; var i= 0; var c=c1=c2=0; // Perform byte-order check. if( dotNetBytes.length >= 3 ) { if( (dotNetBytes[0] & 0xef) == 0xef && (dotNetBytes[1] & 0xbb) == 0xbb && (dotNetBytes[2] & 0xbf) == 0xbf ) { // Hmm byte stream has a BOM at the start, we'll skip this. i= 3; } } while( i < dotNetBytes.length ) { c= dotNetBytes[i]&0xff; if( c < 128 ) { result+= String.fromCharCode(c); i++; } else if( (c > 191) && (c < 224) ) { if( i+1 >= dotNetBytes.length ) throw "Un-expected encoding error, UTF-8 stream truncated, or incorrect"; c2= dotNetBytes[i+1]&0xff; result+= String.fromCharCode( ((c&31)<<6) | (c2&63) ); i+=2; } else { if( i+2 >= dotNetBytes.length || i+1 >= dotNetBytes.length ) throw "Un-expected encoding error, UTF-8 stream truncated, or incorrect"; c2= dotNetBytes[i+1]&0xff; c3= dotNetBytes[i+2]&0xff; result+= String.fromCharCode( ((c&15)<<12) | ((c2&63)<<6) | (c3&63) ); i+=3; } } return result; } function getUTF8CharLength(nChar) { return nChar < 0x80 ? 1 : nChar < 0x800 ? 2 : nChar < 0x10000 ? 3 : nChar < 0x200000 ? 4 : nChar < 0x4000000 ? 5 : 6; } function loadUTF8CharCode(aChars, nIdx) { var nLen = aChars.length, nPart = aChars[nIdx]; return nPart > 251 && nPart < 254 && nIdx + 5 < nLen ? /* (nPart - 252 << 32) is not possible in ECMAScript! So...: */ /* six bytes */ (nPart - 252) * 1073741824 + (aChars[nIdx + 1] - 128 << 24) + (aChars[nIdx + 2] - 128 << 18) + (aChars[nIdx + 3] - 128 << 12) + (aChars[nIdx + 4] - 128 << 6) + aChars[nIdx + 5] - 128 : nPart > 247 && nPart < 252 && nIdx + 4 < nLen ? /* five bytes */ (nPart - 248 << 24) + (aChars[nIdx + 1] - 128 << 18) + (aChars[nIdx + 2] - 128 << 12) + (aChars[nIdx + 3] - 128 << 6) + aChars[nIdx + 4] - 128 : nPart > 239 && nPart < 248 && nIdx + 3 < nLen ? /* four bytes */(nPart - 240 << 18) + (aChars[nIdx + 1] - 128 << 12) + (aChars[nIdx + 2] - 128 << 6) + aChars[nIdx + 3] - 128 : nPart > 223 && nPart < 240 && nIdx + 2 < nLen ? /* three bytes */ (nPart - 224 << 12) + (aChars[nIdx + 1] - 128 << 6) + aChars[nIdx + 2] - 128 : nPart > 191 && nPart < 224 && nIdx + 1 < nLen ? /* two bytes */ (nPart - 192 << 6) + aChars[nIdx + 1] - 128 : /* one byte */ nPart; } /**This code is learned from: * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays/StringView */ function decodeUTF8ArrayMozilla(rawData) { var sView = ""; for ( var nChr, nLen = rawData.length, nIdx = 0; nIdx < nLen; nIdx += getUTF8CharLength(nChr)) { nChr = loadUTF8CharCode(rawData, nIdx); sView += String.fromCharCode(nChr); } return sView; } }());