12.4.2. WebSocket Connections

A WebSocket is a persistent TCP connection between the browser page and the DataHub data engine. It uses the DHTP command set, giving it similar performance characteristics to a DataHub tunnel. The result is that the web page can interact with the DataHub data at high speeds with very low latencies.

A WebSocket connection requires you to write some JavaScript in your web page to create and maintain the connection to a DataHub instance. The DataHub installation contains some JavaScript files to help with this:

These files are located here (32-bit and 64-bit versions):

C:\Program Files (x86)\Cogent\Cogent DataHub\Plugin\WebServer\html\
                                                              scripts
C:\Program Files\Cogent\Cogent DataHub\Plugin\WebServer\html\scripts

Load these two files using <script/> tags in your HTML file. They provide the WebSocket implementation and the message parser respectively.

The WebSocket connection is asynchronous, meaning that your JavaScript page will send messages due to events on the page, typically user interactions, timers, or changes to the connection state, and will not wait for a response to those messages. Similarly, messages from the DataHub server will arrive at any time. Your JavaScript code is therefore designed as a collection of event handlers that respond to different events from the connection or from the web page.

A typical implementation of a WebSocket connection to the DataHub server looks like this:

Once the connection callbacks and data point change event handlers are defined, the DataHubWebSocket implementation will trigger them based on events on the connection or the arrival of data from the DataHub server.

The DataHubWebSocket class

send (string)

Transmits a LISP-formatted command to the DataHub server. Any command listed in the DataHub command set can be transmitted, so long as the user connecting on this WebSocket has sufficient permission to execute it. For example:

ws.send("(cset \"DataPid:PID1.Sp\"50)");
setAuth(username, password)

Sets the user name and password to be used to authenticate on this connection. This will automatically be sent to the server when the connection is first established. Note that the password is transmitted in plain text, so it is advisable to use an HTTPS connection. For example:

ws.setAuth("admin", "admin");
setHeartbeat(heartbeat, timeout)

Sets the heartbeat and timeout of the connection in milliseconds. Setting both of these to zero will disable the heartbeat. If the heartbeat is disabled it could take a long time to detect some kinds of connectivity failure. The heartbeat should be less than half the timeout. For example:

ws.setHeartbeat(5000,15000);
connect(hostname, port, is_ssl)

Starts the connection process. The connection process continues in the background after this call, and is not complete until either onConnectionSuccess or onConnectionFailure is called. If the connection fails it will automatically re-try every 5 seconds. For example:

ws.connect(location.hostname, location.port, 
           location.protocol == "https:");
disconnect()

Terminates the WebSocket connection and stops the 5-second re-try timer. The DataHubWebSocket will not attempt to connect again until connect() is called again. For example:

ws.disconnect();
addPointHandler(pointName, handlerFunction)

Adds a function to the point change handler table for the specified point name. The universal point name, *, indicates all points. If multiple handlers are defined for a point then all handlers will be called each time the value of the point changes. If a universal handler is defined, it will be called in addition to any specific handlers. The universal handler is called after all specific handlers complete.

The handler function is a JavaScript function accepting a single argument. The argument is an array of the tokens from a (point) message, which contains the same tokens as the (write) message. All tokens are delivered as strings, and should be converted to numbers where appropriate. For example:

ws.addPointHandler("DataPid:PID1.Sp", function(tokens)
{
    console.log (tokens[1] + " = " + tokens[3]);
});
removePointHandler(pointName, handlerFunction)

Removes a point value change handler from the handler table. The handlerFunction must compare as equal (using == comparison) to the handlerFunction originally provided to addPointHandler.

registerPoint(pointName)

Sends a message to the DataHub server to register to be notified of value changes for this point. The point name must be fully qualified, including the domain: prefix. If the WebSocket is not currently connected, the point name will be stored, and it will automatically be registered when the connection is subsequently established. If the point is already registered, this call is ignored. For example:

ws.registerPoint("DataPid:PID1.Mv");
registerDomain(domainName, onceOnly)

Sends a message to the DataHub server to register to be notified for all points in a data domain. If onceOnly is true, the domain is registered such that exactly one value message will be transmitted for each point. If a new point is added to the domain after this call is made, that point will be automatically registered. If onceOnly is false, the DataHub server will transmit the current values of all points immediately, and then transmit any subsequent value changes for each point until the connection is lost or the point is unregistered. If the domain is already registered, this call is ignored. For example:

e.g., ws.registerDomain("DataPid", false);
unregisterPoint(pointName)

Sends a message to the DataHub server indicating that it should not transmit value change notifications for this point. If the point is not currently registered then this call does nothing. For example:

ws.unregisterPoint("DataPid:PID1.Mv");
unregisterDomain(domainName)

Sends a message to the DataHub server indicating that it should not transmit value change notifications for any point in this data domain. For example:

ws.unregisterDomain("DataPid");
escaped(string, isQuoted)

Adds escape sequences to the provided string such that it can be correctly parsed by the DataHub server. If isQuoted is true, the returned string will start and end with double-quote characters. If isQuoted is false, the caller is responsible for adding the double-quote characters to the message being sent to the DataHub server. For example:

ws.escaped('The dog said "woof"!', false) > The dog said \"woof\"!
ws.escaped('The dog said "woof"!', true) > "The dog said \"woof\"!"
ISOlocaltimeStr(date) , ISOlocaldateStr(date) , ISOlocalDTstr(date)

Accepts an argument of type Date, and returns a string representing the ISO-formatted time string, date string, and date+time string respectively representing the local time representation of the provided date. For example:

ws.ISOlocaltimeStr(new Date()) > '09:41:02'
ws.ISOlocaldateStr(new Date()) > '2021-10-26'
ws.ISOlocalDTstr(new Date()) > '2021-10-26 09:41:02'
StampTime(seconds)

Accepts a floating point number of seconds in the Unix epoch (seconds since January 1, 1970) and returns a the local ISO time representation with millisecond accuracy. For example:

ws.StampTime(Date.now()/1000.0)  '09:41:02.452'

The DataHubWebSocket class exposes an array called handlers that allows you to define custom code that will run when any command message arrives from the DataHub server. By default, two handlers are defined, for point and echo messages respectively. You can replace these with your own handlers if you do not want to use the default point handling mechanism.

A command handler is a function taking a single argument. The argument is an array of the tokens in the command produced by parsing the incoming string. The first token in the array is the name of the command.

To add a custom event handler for any message, assign a function to handlers[command]. For example, to add a custom handler for a (domains name1 name2) message arriving from the DataHub server, do:

ws.handlers["domains"] = function (data) { /* do something */ };

A handler can be disabled by setting its function to null.

Complete web page example: WebSocketApp.html

<html>
  <head>
    <title>Simple WebSocket connection to DataHub</title>
    <!-- These two scripts are necessary to make a WebSocket 
    connection to the DataHub server -->
    <script src="/scripts/DHWebSocket.js"></script>
    <script src="/scripts/lispparse.js"></script>
  </head>
  <body>
    <h1>Simple WebSocket connection to DataHub</h1>
    <h3>Connection messages:</h3>
    <div id="connlog"></div>
    <h3>Data Values:</h3>
    <div id="datalog"></div>
    <script>
    // Collect a user name and password somehow from 
    // the user, if necessary.
    var username = "";
    var password = "";
            
    ws = new DataHubWebSocket();
    ws.setAuth(username, password);
    ws.setHeartbeat(5000, 30000);
            
    // This function will be called if the connection succeeds. 
    // It will be called every time the connection is established or 
    // re-established after a connection loss. You should treat all 
    // connections as being fresh - the DataHub server will not 
    // maintain state after a connection loss.  This means that any 
    // data point  registrations and other setup commands must be 
    // re-transmitted each time.
    ws.onConnectionSuccess = function (host, port) 
    {
        logConnection("Connection succeeded");
                
        // This code will register for the current values of all 
        // points in the DataPid domain, but not for future changes.
        ws.registerDomain("DataPid", true);
                
        // Request a list of all data domains.  The return will be 
        // handled in the handler for the response message, 
        // (domains domain1 domain2 ...)
        ws.send("(domains)");
    }
            
    // This function will be called each time a connection attempt
    // fails, and each time that a previously  successful connection 
    // is disconnected.
    ws.onConnectionFailure = function (host, port) 
    {
        logConnection("Connection failed to " 
                       + host + ":" + port);
    }
            
    // During onConnectionSuccess above we sent a (domains) 
    // command.  This handles the response when it arrives.
    ws.handlers[ "domains"] = function (data) 
    {
        logConnection("Domains: " + data);
                
        // This code will register for point changes from all 
        // domains, including all newly created points and 
        // all new values of every point as they change.
        for (var i = 1; i < data.length; i++) 
        {
            ws.registerDomain(data[i], false);
        }
    }
            
    // When a message arrives and does not have a specific handler, 
    // it will execute the special AsyncMessage handler instead.
    ws.handlers[ "AsyncMessage"] = function (data) 
    {
        logConnection("AsyncMessage: " + data);
    }
            
    // Ignore (alive) messages.  They are produced when the 
    // connection is idle and a heartbeat time has been set.
    ws.handlers[ "alive"] = function (data) 
    {
        logConnection("Received alive message");
    };
            
    // Create a point change handler that logs the point value.
    function pointHandler (data) 
    {
        logData("Point: " + data);
    }
            
    // Add the point change handler to all points.  We could use a
    // fully-qualified point name (like "DataPid:PID.Mv") instead 
    // of "*" here to call the above handler only for that point. 
    // If there is a "*" handler it will be also be called after 
    // any point-specific handlers.
    ws.addPointHandler("*", pointHandler);
            
    // Logging functions that distinguish connection messages 
    // from data value messages.  They limit the size of the 
    // message history to 4096 bytes.
            
    var maxMessageSize = 4096;
            
    function log(elementId, message) 
    {
        var logTag = document.getElementById(elementId);
        var str = logTag.innerHTML;
                
        str += "[" + ws.StampTime(Date.now() / 1000) + 
               "] " + message + "<br/>\n";
        if (str.length > maxMessageSize) 
        {
            var pos = str.indexOf("\n", str.length - maxMessageSize);
            str = str.substring(pos);
        }
        logTag.innerHTML = str;
    }
            
    function logConnection(message) 
    {
        log("connlog", message);
    }
            
    function logData(message) 
    {
        log("datalog", message);
    }
            
    // Finally, start the connection process.  The connection is not
    // actually made during this call.  Instead an asynchronous con- 
    // nection attempt is made, and we will be notified of its success 
    // or failure in onConnectionSuccess or onConnectionFailure.
    ws.connect(location.hostname, location.port, 
               location.protocol == "https:");
    </script>
  </body>
</html>