/*******************************************************************************
* 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;
}
}());