Source: WebSocketDevice.js

//*********************************************
// Connect to the device , send, receive messages
//*********************************************

var CloverID = require("./CloverID.js");
var CloverError = require("./CloverError.js");
var EventEmitter = require("eventemitter3");
var XmlHttpSupport = require("./xmlHttpSupport.js");

var RemoteMessageBuilder = require("./RemoteMessageBuilder.js");

var Logger = require('./Logger.js');

/**
 * The object used to communicate with the device.
 *
 *
 * @constructor
 */
function WebSocketDevice(allowOvertakeConnection, friendlyId) {
    this.log = Logger.create();
    // This is the websocket connection
    this.deviceSocket = null;
    // The id for the ping interval timer so we can stop it
    this.pingIntervalId = null;
    // flag for echoing messages to the console
    this.echoAllMessages = false;
    // The last time a pong was received, set to current time initially
    this.pongReceivedMillis = new Date().getTime();
    // The last time a ping was sent, set to current time initially
    this.pingSentMillis = new Date().getTime();
    // How often a ping is sent
    this.millisecondsBetweenPings = 5000; // 5 seconds
    // How long should it be before we warn on a dead connection
    this.deadConnectionWarnThreshold = this.millisecondsBetweenPings * 2;
    // How long should it be before we error on a dead connection
    this.deadConnectionErrorThreshold = this.deadConnectionWarnThreshold * 4;
    // How long should it be before we shut down on a dead connection
    this.deadConnectionShutdownThreshold = this.deadConnectionErrorThreshold * 2;
    this.allowOvertakeConnection = allowOvertakeConnection;
    this.friendlyId = CloverID.getNewId();
    if(friendlyId) {
        this.friendlyId = friendlyId;
    }
    // Flag to indicate if we attempt reconnects
    this.reconnect = true;
    this.reconnectAttempts = 0;
    this.numberOfReconnectAttemptsBeforeWeGiveUp = 5;
    this.timebetweenReconnectAttempts = 6000;
    // A queue of messages that may be populated while we attempt to reconnect.
    this.resendQueue = [];

    // Used to emit messages and state of the device
    this.eventEmitter = new EventEmitter();

    /**
     * Initiates contact with the device.
     *
     * @param {url} ws_address - the web service url to connect to to communicate with the clover device
     */
    this.contactDevice = function(ws_address) {
        this.baseAddress = ws_address;
        this.reContactDevice(this.generateAddress(this.baseAddress));
    };

    this.generateAddress = function(baseAddress) {
        var connect = "?";
        if(baseAddress.indexOf("?") > -1){
            connect = "&";
        }
        var generatedAddress = baseAddress + connect + "friendlyId=" + this.friendlyId;
        if(this.allowOvertakeConnection) {
            generatedAddress = generatedAddress + connect + "forceConnect=true";
        } else {
            generatedAddress = generatedAddress + connect + "forceConnect=false";
        }
        return generatedAddress;
    };
    /**
     * Initiates contact with the device.
     *
     * @param {url} ws_address - the web service url to connect to to communicate with the clover device
     */
    this.reContactDevice = function(ws_address) {
        var me = this;
        // Reset the timestamp values
        // this.pongReceivedMillis = new Date().getTime();
        this.pingSentMillis = new Date().getTime();

        /*
         * Start the websocket connection to the device
         */
        if (ws_address) {
            // This will be a connection to the clover POSApiServer/SupportServer.  The url
            // can include the device id to contact, and let the server determine how to get
            // to the device (it is doing that now , but it is hardcoded)
            // deviceSocket = new WebSocket("ws://localhost:18000");
            try {
                me.log.debug("contacting device");

                // A different way to deal with the 401 error that
                // occurs when a websocket connection is made to the
                // server (sometimes).  Do a preliminary OPTIONS
                // request.  Although this happens regarless of if the error
                // happens, it is tremendously faster.
                var wssUrl = ws_address;// me.deviceSocket.url;

                var httpUrl = null;
                if (wssUrl.indexOf("wss") > -1) {
                    httpUrl = wssUrl.replace("wss", "https");
                } else {
                    httpUrl = ws_address.replace("ws", "http");
                }

                if (!me.xmlHttpSupport) {
                    me.xmlHttpSupport = new XmlHttpSupport();
                }
                me.xmlHttpSupport.options(httpUrl,
                    function () {me.startSSSS(ws_address)},
                    function () {me.startSSSS(ws_address)}
                    );
            } catch (error) {
                me.log.error(error);
            }
        }
    };

    this.startSSSS = function(ws_address) {
        var me = this;
        // See com.clover.support.handler.remote_pay.RemotePayConnectionControlHandler#X_CLOVER_CONNECTED_ID
        var connectedId = me.xmlHttpSupport.getResponseHeader("X-CLOVER-CONNECTED-ID");

        if (connectedId && !this.allowOvertakeConnection) {
            if (this.friendlyId == connectedId) {
                // Do anything here?  This is already connected.
                this.log.debug("Trying to connect, but already connected.");
            } else {
                this.connectionDenied(connectedId);
                return;
            }
            if (this.deviceSocket && this.deviceSocket.readyState == WebSocket.OPEN) {
                return;
            }
        }

        if (!this.deviceSocket || this.deviceSocket.readyState != WebSocket.OPEN) {
            this.log.debug("Contacting url - " + ws_address);
            this.deviceSocket = new WebSocket(
                //        "ws://192.168.0.56:49152"
                //        selectedDevice.websocket_direct
                ws_address
            );
            this.log.debug("this.deviceSocket = " + this.deviceSocket);
            this.deviceSocket.onopen = function (event) {
                me.log.debug("deviceSocket.onopen");
                me.reconnecting = false;
                me.reconnectAttempts = 0;
                // Set up the ping for every X seconds
                if(me.pingIntervalId) {
                    clearInterval(me.pingIntervalId);
                }
                me.pingIntervalId = setInterval(function () {
                    me.checkDeadConnection();
                    me.ping();
                }, me.millisecondsBetweenPings);
                me.onopen(event);
            };

            this.deviceSocket.onmessage = function (event) {
                var jsonMessage = JSON.parse(event.data);
                me.receiveMessage(jsonMessage);
            };

            this.deviceSocket.onerror = function (event) {
                me.log.error(event);
                if (me.reconnect) {
                    me.startupReconnect(me.timebetweenReconnectAttempts);
                }
                else {
                    me.onerror(event);
                }
            };

            this.deviceSocket.onclose = function (event) {
                try {
                    me.log.debug("Clearing ping thread");
                    if(me.pingIntervalId) {
                        clearInterval(me.pingIntervalId);
                    }
                } catch (e) {
                    me.log.error(e);
                }
                me.onclose(event);
            }
        }
    };


    this.startupReconnect = function(delay) {
        if(!delay)delay=1;
        if (this.reconnectAttempts < this.numberOfReconnectAttemptsBeforeWeGiveUp) {
            this.reconnectAttempts++;
            setTimeout(
                function () {
                    this.reconnectAttempts++;
                    this.attemptReconnect();
                }.bind(this), delay);
        }
        else {
            this.log.error("Exceeded number of reconnect attempts, giving up. There are " +
                this.resendQueue.length + " messages that were queued, but not sent.");
            this.reconnectAttempts = 0;
            this.onerror(event);
        }
    };

    /**
     * Called to check the state of the connection.
     *
     * @private
     */
    this.checkDeadConnection  = function() {
        if(this.pongReceivedMillis < this.pingSentMillis) {
            var currentMillis = new Date().getTime();
            var lag = (currentMillis - this.pongReceivedMillis);
            if(lag > this.deadConnectionWarnThreshold) {
                if(lag > this.deadConnectionErrorThreshold) {
                    this.pongReceivedMillis = new Date().getTime();
                    this.connectionError(lag);
                }
                else {
                    this.connectionWarning(lag);
                }
            }
        }
        else {
            this.connectionOK();
        }
    };

    /**
     * Called when an initial connection is actively denied, typically because the
     * device is already paired to another terminal.
     *
     * @param terminalIdPairedTo
     */
    this.connectionDenied = function(terminalIdPairedTo) {
        this.eventEmitter.emit(WebSocketDevice.CONNECTION_DENIED, terminalIdPairedTo);
        this.eventEmitter.emit(WebSocketDevice.ALL_MESSAGES, terminalIdPairedTo);
    };

    /**
     * Called when the connection is OK.
     * Emits the
     *  WebSocketDevice.CONNECTION_OK message to registered listeners.
     */
    this.connectionOK = function() {
        var message = "Connection Ok";
        this.eventEmitter.emit(WebSocketDevice.CONNECTION_OK, message);
        this.eventEmitter.emit(WebSocketDevice.ALL_MESSAGES, message);
    };

    /**
     * The connection was taken away from us.  Do NOT try to reconnect!
     *
     * @param message
     */
    this.connectionStolen = function(message) {
        this.eventEmitter.emit(WebSocketDevice.CONNECTION_STOLEN, message);
        this.eventEmitter.emit(WebSocketDevice.ALL_MESSAGES, message);
        this.forceClose(true);
    };

    /**
     * Called when the lag on communication has reached an error length
     * Emits the
     *  WebSocketDevice.CONNECTION_ERROR message to registered listeners.
     * @param {Number} lag - the number of milliseconds between communication to and from the device.  Measured as
     *  related to 'pong' responses to a 'ping'
     */
    this.connectionError = function(lag) {
        var message = "Connection appears to be dead...no response in " + lag + " milliseconds";
        // Protect users from themselves.  If the connection lag has exceeded an absolute maximum
        // without response, shut it down.
        if(lag > this.deadConnectionShutdownThreshold) {
            message += "  This exceeds the system maximum wait, shutting down.";
            this.forceClose();
        }
        this.eventEmitter.emit(WebSocketDevice.CONNECTION_ERROR, message);
        this.eventEmitter.emit(WebSocketDevice.ALL_MESSAGES, message);
    };

    /**
     * Called when the lag on communication has reached a warning length
     * Emits the
     *  WebSocketDevice.CONNECTION_WARNING message to registered listeners.
     * @param {Number} lag - the number of milliseconds between communication to and from the device.  Measured as
     *  related to 'pong' responses to a 'ping'
     */
    this.connectionWarning = function(lag) {
        var message = "Connection is slow...no response in " + lag + " milliseconds";
        this.eventEmitter.emit(WebSocketDevice.CONNECTION_WARNING, message);
        this.eventEmitter.emit(WebSocketDevice.ALL_MESSAGES, message);
    };

    /**
     * Called on device error
     * Emits the
     *  WebSocketDevice.DEVICE_ERROR event to registered listeners.
     * @param event
     */
    this.onerror = function(event) {
        this.eventEmitter.emit(WebSocketDevice.DEVICE_ERROR, event);
        this.eventEmitter.emit(WebSocketDevice.ALL_MESSAGES, event);
    };

    /**
     * Called when the device is opened
     * Emits the
     *  WebSocketDevice.DEVICE_OPEN event to registered listeners.
     * @param event
     */
    this.onopen = function(event) {
        this.eventEmitter.emit(WebSocketDevice.DEVICE_OPEN, event);
        this.eventEmitter.emit(WebSocketDevice.ALL_MESSAGES, event);
    };

    /**
     * Called when the device is closed
     * Emits the
     *  WebSocketDevice.DEVICE_CLOSE event to registered listeners.
     * @param event
     */
    this.onclose = function(event) {
        this.eventEmitter.emit(WebSocketDevice.DEVICE_CLOSE, event);
        this.eventEmitter.emit(WebSocketDevice.ALL_MESSAGES, event);
    };

    /**
     * Called to initiate disconnect from the device
     */
    this.disconnectFromDevice = function() {
        this.reconnect = false;
        this.forceClose();
    };

    /**
     * Do not really want to ever have to do this, but it is
     * sometimes needed.  The above #WebSocketDevice.disconnectFromDevice
     * is how this should be closed.  That sends a message to the device
     * to tell it that we are closing.  But this may be needed if the
     * device is not responsive.
     *
     * @param skipSendShutdown - if true, then the shutdown message is NOT
     *  sent to the device.
     */
    this.forceClose = function (skipSendShutdown) {
        try {
            clearInterval(this.pingIntervalId);
        } catch (e) {
        }
        if(!skipSendShutdown) {
            var oldReconnect = this.reconnect;
            this.reconnect = false;
            try {
                this.sendShutdown();
            } catch (e) {
            }
            this.reconnect = oldReconnect;
        }
        setTimeout(function() {
            if (this.deviceSocket) {
                try {
                    this.deviceSocket.close();
                } catch (e) {
                }
            }
        }.bind(this), 2000);
    };

    /**
     * Called to attempt to reconnect ot the device
     * @private
     */
    this.attemptReconnect = function() {
        {
            // Disconnect without telling the peer that we
            // wantto, because it appears
            // the connection may have gone stale.
            clearInterval(this.pingIntervalId);
            if(this.deviceSocket.readyState == WebSocket.OPEN) {
                this.deviceSocket.close();
            }
        }
        this.reconnecting = true;
        this.log.debug("attempting reconnect...");
        var me = this;
        if (this["bootStrapReconnect"]) {
            // Depending on the configuration, we may be able to tell the device to wake
            // up.
            this["bootStrapReconnect"](
                function() {
                    me.reContactDevice(me.generateAddress(me.baseAddress));
                }
            );
        } else {
            // Cannot tell device to wake up, we do not have a bootstrap/notification
            // configuration for it. It may require that the user starts the cloud pay display
            this.reContactDevice(this.generateAddress(this.baseAddress));
        }
    };

    /**
     * Send a message on the websocket.  The parameter will be serialized to json
     * @param {Object} message - the message to send
     */
    this.sendMessage = function(message) {
        var stringMessage = message==null?null:JSON.stringify(message);
        if(this.echoAllMessages) {
            this.log.debug("sending message:" + stringMessage)
        }
        // If the deviceSocket is closed or closing, we may try to reconnect
        if(this.deviceSocket.readyState == WebSocket.CLOSING || this.deviceSocket.readyState == WebSocket.CLOSED) {
            // If we are set up to try to reconnect
            if(this.reconnect) {
                // Push the message we just got on a resend queue
                if (stringMessage) {
                    this.resendQueue.push(stringMessage);
                }
                // If we are not already trying to reconnect, try now
                if(!this.reconnecting) {
                    this.attemptReconnect();
                }
            }
            else {
                this.disconnectFromDevice();
                throw new CloverError(CloverError.DEVICE_OFFLINE, "Device disconnected, message not sent: " +
                  stringMessage );
            }
        }
        else {
            // If the websocket is not completely connected yet, put the message on the queue and call sendMessage again
            // with a null message.  That way the message order will be correct.  If another message comes through
            // before the timeout expires, the queue will be drained first, so the message will go throgh in order, and
            // the send that results from the timeout will do nothing.
            if (this.deviceSocket.readyState == WebSocket.CONNECTING) {
                // The message is not null, put it on the queue and call
                // send again.
                if (stringMessage) {
                    this.resendQueue.push(stringMessage);
                    var me = this;
                    setTimeout(me.sendMessage.bind(me), me.millisecondsBetweenPings);
                }
            } else {
                // If there is anything in the resend queue, send it now.
                while (this.resendQueue.length > 0) {
                    this.deviceSocket.send(this.resendQueue.shift());
                }
                // If a message was passed, send it.
                if (stringMessage) {
                    this.deviceSocket.send(stringMessage);
                }
            }
        }
    };

    /**
     * Get a message on the websocket.
     * @param {Object} message - the message received on the socket
     * @private
     */
    this.receiveMessage = function(message) {
        if(this.echoAllMessages) {
            var stringMessage = JSON.stringify(message);
            this.log.debug("receive message:" + stringMessage)
        }
        if(message.hasOwnProperty("type")) {
            if(message["type"] == RemoteMessageBuilder.PONG) {
                this.pongReceived(message);
            } else if(message["type"] == RemoteMessageBuilder.PING) {
                this.pingReceived(message);
            } if(message["type"] == RemoteMessageBuilder.FORCE) {
                this.connectionStolen(message);
            }
        }
        if(message.hasOwnProperty("method")) {
            // note: Look at JSON.parse(text[, reviver]).
            // This can be used with the MethodToMessage class
            // to automatically parse the json into the correct objects.
            this.eventEmitter.emit(message.method, message);
        }
        this.eventEmitter.emit(WebSocketDevice.ALL_MESSAGES, message);
    };

    /**
     * Registers event callbacks for message method types, and device state.
     *
     * @see LanMethod
     *
     * @param {string} eventName - one of the LanMethod types, or one of the
     *  LOCAL_EVENT types for device state.
     * @param {function} callback - the function called with the event data
     */
    this.on = function (eventName, callback) {
        this.eventEmitter.on(eventName, callback);
    };

    /**
     * Unregisters an event callback.
     *
     * @param eventName
     * @param callback
     */
    this.removeListener = function (eventName, callback) {
        this.eventEmitter.removeListener(eventName, callback);
    };

    /**
     * Unregisters a set of event callbacks.
     *
     * @param {Array} listeners - an array of objects of the form {"event":LanMethod.FINISH_OK, "callback":finishOKCB}
     */
    this.removeListeners = function (listeners) {
        for(var idx=0;idx<listeners.length;idx++){
            this.removeListener(listeners[idx].event, listeners[idx.callback]);
        }
    };

    /**
     * Registers event callbacks for message method types, and device state. The callback will be
     * called at most once.
     *
     * @see LanMethod
     *
     * @param {string} eventName - one of the LanMethod types, or one of the
     *  LOCAL_EVENT types for device state.
     * @param {function} callback - the function called with the event data
     */
    this.once = function (eventName, callback) {
        this.eventEmitter.once(eventName, callback);
    };

    /**
     * @private
     */
    this.pongReceived = function() {
        this.pongReceivedMillis = new Date().getTime();
    };

    /**
     * @private
     */
    this.pingReceived = function() {
        this.pong();
    };

    /**
     * @private
     */
    this.pong = function() {
        this.pingSentMillis = new Date().getTime();
        this.sendMessage(this.messageBuilder.buildPong());
    };

    /**
     * @private
     */
    this.ping = function() {
        this.pingSentMillis = new Date().getTime();
        this.sendMessage(this.messageBuilder.buildPing());
    }
}

/**
 * Special message method type used to receive all messages received by this device interface.  Used when registering
 * a callback to the WebSocketDevice.on function
 */
WebSocketDevice.ALL_MESSAGES = "ALL_MESSAGES";

// Device state events
/**
 * Prefix for events that are local to the device - right now just
 * for device state events.
 * @type {string}
 */
WebSocketDevice.LOCAL_EVENT = "LOCAL_EVENT";

/**
 * Event emitter key for connection stolen messages
 * @type {string}
 */
WebSocketDevice.CONNECTION_STOLEN = WebSocketDevice.LOCAL_EVENT + "_CONNECTION_STOLEN";
/**
 * Event emitter key for connection denied messages
 * @type {string}
 */
WebSocketDevice.CONNECTION_DENIED = WebSocketDevice.LOCAL_EVENT + "_CONNECTION_DENIED";
/**
 * Event emitter key for connection ok messages
 * @type {string}
 */
WebSocketDevice.CONNECTION_OK = WebSocketDevice.LOCAL_EVENT + "_CONNECTION_OK";
/**
 * Event emitter key for connection error messages
 * @type {string}
 */
WebSocketDevice.CONNECTION_ERROR = WebSocketDevice.LOCAL_EVENT + "_CONNECTION_ERROR";
/**
 * Event emitter key for connection warning messages
 * @type {string}
 */
WebSocketDevice.CONNECTION_WARNING = WebSocketDevice.LOCAL_EVENT + "_CONNECTION_WARNING";
/**
 * Event emitter key for device error events
 * @type {string}
 */
WebSocketDevice.DEVICE_ERROR = WebSocketDevice.LOCAL_EVENT + "_DEVICE_ERROR";
/**
 * Event emitter key for device open events
 * @type {string}
 */
WebSocketDevice.DEVICE_OPEN = WebSocketDevice.LOCAL_EVENT + "_DEVICE_OPEN";
/**
 * Event emitter key for device close events
 * @type {string}
 */
WebSocketDevice.DEVICE_CLOSE = WebSocketDevice.LOCAL_EVENT + "_DEVICE_CLOSE";


//**************************************************************
// Functionality to deal with sending messages
//**************************************************************


// Send an update to the order to the device.  No idea what the update is,
// this is just the comms
/**
 * TElls the device to display the passed order.
 *
 * @param {Object} order - the entire order json object
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendShowOrderScreen = function(order, ackId) {
    var payload = {
        "order": JSON.stringify(order)
    };
    var lanMessage = this.messageBuilder.buildShowOrderScreen(payload);
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) {
        lanMessage.id = ackId;
    }

    this.sendMessage(lanMessage);
};

/**
 * @typedef {Object} Operation
 * @property {string} orderId - the id of the order the operation is on
 * @property {StringArray} ids - a array container of ids for the order operation
 */

/**
 * @typedef {Object} StringArray
 * @property {string[]} elements - string elements
 */

/**
 * Sends an update to the order to the device, and causes it to display the change.
 *
 * @param {Object} order - the entire order json object
 * @param {Operation} lineItemsAddedOperation
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendShowOrderLineItemAdded = function(order, lineItemsAddedOperation, ackId) {
    var payload = {
        "order": JSON.stringify(order),
        "lineItemsAddedOperation": JSON.stringify(lineItemsAddedOperation)
    };
    var lanMessage = this.messageBuilder.buildShowOrderScreen(payload);
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};

/**
 * Sends an update to the order to the device, and causes it to display the change.
 *
 * @param {Object} order - the entire order json object
 * @param {Operation} lineItemsDeletedOperation
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendShowOrderLineItemRemoved = function(order, lineItemsDeletedOperation, ackId) {
    var payload = {
        "order": JSON.stringify(order),
        "lineItemsDeletedOperation": JSON.stringify(lineItemsDeletedOperation)
    };
    var lanMessage = this.messageBuilder.buildShowOrderScreen(payload);
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};

/**
 * Sends an update to the order to the device, and causes it to display the change.
 *
 * @param {Object} order - the entire order json object
 * @param {Operation} discountsAddedOperation
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendShowOrderDiscountAdded = function(order, discountsAddedOperation, ackId) {
    var payload = {
        "order": JSON.stringify(order),
        "discountsAddedOperation": JSON.stringify(discountsAddedOperation)
    };
    var lanMessage = this.messageBuilder.buildShowOrderScreen(payload);
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};

/**
 * Sends an update to the order to the device, and causes it to display the change.
 *
 * @param {Object} order - the entire order json object
 * @param {Operation} discountsDeletedOperation
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendShowOrderDiscountRemoved = function(order, discountsDeletedOperation, ackId) {
    var payload = {
        "order": JSON.stringify(order),
        "discountsDeletedOperation": JSON.stringify(discountsDeletedOperation)
    };
    var lanMessage = this.messageBuilder.buildShowOrderScreen(payload);
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};

/**
 *
 * @param {KeyPress} keyCode - the KeyPress to send to the device
 * @param [ackId] - an optional id for the message.  If set then the keypress will be acknowledged,
 *  otherwise there will be no response.
 */
WebSocketDevice.prototype.sendKeyPress = function(keyCode, ackId) {
    var payload = {
        "keyPress": keyCode
    };
    var lanMessage = this.messageBuilder.buildKeyPress(payload);
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};


/**
 *
 * @param [ackId] - an optional id for the message.  If set then the break will be acknowledged,
 *  otherwise there will be no response.
 */
WebSocketDevice.prototype.sendBreak = function(ackId) {
    var payload = {

    };
    var lanMessage = this.messageBuilder.buildBreak(payload);
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};


//
/**
 * Send a message to start a transaction.  This will make the device display the payment screen
 *
 * @param {Object} payIntent - the payment intention object
 * @param {boolean} suppressOnScreenTips
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendTXStart = function(payIntent, suppressOnScreenTips, ackId) {

    // This is how they are doing the payload...
    var payload = {
        "payIntent": payIntent,
        "suppressOnScreenTips": suppressOnScreenTips
    };

    var lanMessage = this.messageBuilder.buildTxStart(payload);
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};

//
/**
 * Verify that the signature is valid
 *
 * @param {Object} payment - the payment object with signature verification fields populated (positively)
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendSignatureVerified = function(payment, ackId) {
    var payload = {};
    payload.verified = true;
    payload.payment = JSON.stringify(payment);

    var lanMessage = this.messageBuilder.buildSignatureVerified(payload);
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};

// Reject the signature
/**
 * Verify that the signature is NOT valid
 *
 * @param {Object} payment - the payment object with signature verification fields populated (negatively)
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendSignatureRejected = function(payment, ackId) {
    var payload = {};
    payload.verified = false;
    payload.payment = JSON.stringify(payment);

    var lanMessage = this.messageBuilder.buildSignatureVerified(payload);
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};

/**
 * Void a payment
 *
 * @param {Object} payment - the payment object with signature verification fields populated (negatively)
 * @param voidReason
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendVoidPayment = function(payment, voidReason, ackId) {
    var payload = {};
    payload.payment = JSON.stringify(payment);
    payload.voidReason = voidReason;

    var lanMessage = this.messageBuilder.buildVoidPayment(payload);
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};

/**
 * Vault a card
 *
 * @param {int} cardEntryMethods - card entry methods, bitwise OR of {@link CardEntryMethods} constants
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendVaultCard = function(cardEntryMethods, ackId) {
    var payload = {};
    payload.cardEntryMethods = cardEntryMethods;

    var lanMessage = this.messageBuilder.buildVaultCard(payload);
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};

/**
 * Refund a payment, partial or complete
 *
 * @param {string} orderId - the id for the order the refund is against
 * @param {string} paymentId - the id for the payment on the order the refund is against
 * @param {number} [amount] - the amount that will be refunded.  If not included, the amount of
 *  the passed payment will be refunded.
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendRefund = function(orderId, paymentId, amount, ackId) {
    var payload = {};
    payload.orderId = orderId;
    payload.paymentId = paymentId;
    if(amount)payload.amount = amount;

    var lanMessage = this.messageBuilder.buildRefund(payload);
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};

/**
 * Refund a payment, partial or complete
 *
 * @param {string} orderId - the id for the order the refund is against
 * @param {string} paymentId - the id for the payment on the order the refund is against
 * @param {number} [amount] - the amount that will be refunded.
 * @param {boolean} [fullRefund] - If true, the amount of the passed payment will be refunded.
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendRefundV2 = function(orderId, paymentId, amount, fullRefund, ackId) {
    var payload = {};
    payload.orderId = orderId;
    payload.paymentId = paymentId;
    if(fullRefund) {
        payload.fullRefund = fullRefund;
    } else if(amount) {
        payload.amount = amount;
    }
    payload.version = 2;
    var lanMessage = this.messageBuilder.buildRefund(payload);
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};

/**
 * Capture a preauthorization
 *
 * @param {string} orderId - the id for the order the payment was against
 * @param {string} paymentId - the id for the payment on the order the preauth is against
 * @param {number} amount - the final amount for the payment, not including the tip
 * @param {number} [tipAmount] - the tip for the order
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendCapturePreAuth = function(orderId, paymentId, amount, tipAmount, ackId) {
    var payload = {};
    payload.orderId = orderId;
    payload.paymentId = paymentId;
    if(amount)payload.amount = amount;
    if(tipAmount)payload.tipAmount = tipAmount;

    var lanMessage = this.messageBuilder.buildCapturePreAuth(payload);
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};



/**
 * Capture a preauthorization
 *
 * @param allowOpenTabs
 * @param batchId
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendCloseout = function(allowOpenTabs, batchId, ackId) {
    var payload = {};
    payload.allowOpenTabs = allowOpenTabs;
    payload.batchId = batchId;

    var lanMessage = this.messageBuilder.buildCloseout(payload);
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};


/**
 * Adjust a payment
 *
 * @param {string} orderId - the id for the order the adjust is against
 * @param {string} paymentId - the id for the payment on the order the adjust is against
 * @param {number} tipAmount - the amount that will be adjusted.
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendTipAdjust = function(orderId, paymentId, tipAmount, ackId) {
    var payload = {};
    payload.orderId = orderId;
    payload.paymentId = paymentId;
    payload.tipAmount = tipAmount;

    var lanMessage = this.messageBuilder.buildTipAdjust(payload);
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};

/**
 * Show the receipt options screen for a specific orderid/paymentid.
 *
 * @deprecated - this function will result in a Finish_OK message being returned.
 *  Use sendShowPaymentReceiptOptionsV2 instead.  sendShowPaymentReceiptOptionsV2
 *  results in only the UI_STATE messages, which is preferable.
 * @param {string} orderId - the id for the order
 * @param {string} paymentId - the id for the payment on the order
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendShowPaymentReceiptOptions = function(orderId, paymentId, ackId) {
    var payload = {};
    payload.orderId = orderId;
    payload.paymentId = paymentId;

    var lanMessage = this.messageBuilder.buildShowPaymentReceiptOptions(payload);
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};

/**
 * Show the receipt options screen for a specific orderid/paymentid.
 *
 * @param {string} orderId - the id for the order
 * @param {string} paymentId - the id for the payment on the order
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendShowPaymentReceiptOptionsV2 = function(orderId, paymentId, ackId) {
    var payload = {};
    payload.orderId = orderId;
    payload.paymentId = paymentId;
    payload.version = 2;

    var lanMessage = this.messageBuilder.buildShowPaymentReceiptOptions(payload);
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};

/**
 * Open the cash drawer (if one is connected).
 *
 * @param {string} reason - the reason the drawer was opened
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendOpenCashDrawer = function(reason, ackId) {
    var payload = {};
    payload.reason = reason;

    var lanMessage = this.messageBuilder.buildOpenCashDrawer(payload);
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};

/**
 * Send a cancellation message
 *
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendFinishCancel = function(ackId) {
    var lanMessage = this.messageBuilder.buildFinishCancel();
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};

/**
 * Send a message to show the 'Thank You' screen
 *
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendShowThankYouScreen = function(ackId) {
    var lanMessage = this.messageBuilder.buildShowThankYouScreen();
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};

/**
 * Send a message to show the 'Welcome' screen
 *
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendShowWelcomeScreen = function(ackId) {
    var lanMessage = this.messageBuilder.buildShowWelcomeScreen();
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};

/**
 * Send a message to show a custom message on the screen
 *
 * @param {string} message - the message to display
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendTerminalMessage = function(message, ackId) {
    var payload = {"text" : message};
    var lanMessage = this.messageBuilder.buildTerminalMessage(payload);
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};

/**
 * Send a message to ask the device if it is there.
 *
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendDiscoveryRequest = function(ackId) {
    var lanMessage = this.messageBuilder.buildDiscoveryRequest();
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};

/**
 * Send a message to ask the device to print some text
 *
 * @param textLines - an  array of strings
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendPrintText = function(textLines, ackId) {
    //List<String> textLines
    var payload = {"textLines" : textLines};
    var lanMessage = this.messageBuilder.buildPrintText(payload);
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};

/**
 * Send a message to ask the device to shutdown the connection.
 */
WebSocketDevice.prototype.sendShutdown = function() {
    var lanMessage = this.messageBuilder.buildShutdown();
    // Note:  the 'ack' is not included here because the device will
    // be shutting down,
    this.sendMessage(lanMessage);
};

/**
 * Send a message with an image for the device to print.
 *
 * @param img - an image.  Can be obtained in a manner similar to :
 *  <pre>var img = document.getElementById("img_id");</pre>
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendPrintImage = function(img, ackId) {
    var payload = {"png" : this.getBase64Image(img) };
    var lanMessage = this.messageBuilder.buildPrintImage(payload);
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};


/**
 * Send a message with an image url for the device to print.
 *
 * @param urlString - a url to an image that is reachable from the device
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendPrintImageFromURL = function(urlString, ackId) {
    var payload = {"urlString" : urlString };
    var lanMessage = this.messageBuilder.buildPrintImage(payload);
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};

/**
 * Send a message to the device to get the last message it received, along with the response
 * returned (if any)
 *
 * @param {string} [ackId] - an optional identifier that can be used to track an acknowledgement
 *  to this message.  This should be a unique identifier, but this is NOT enforced in any way.
 *  A "ACK" message will be returned with this identifier as the message id if this
 *  parameter is included.  This "ACK" message will be in addition to any other message
 *  that may be generated as a result of this message being sent.
 */
WebSocketDevice.prototype.sendLastMessageRequest = function(ackId) {
    var payload = {};
    var lanMessage = this.messageBuilder.buildLastMessageRequest(payload);
    // If an id is included, then an "ACK" message will be sent for this message
    if(ackId) lanMessage.id = ackId;

    this.sendMessage(lanMessage);
};


/**
 * @private
 * @param img
 * @returns {string}
 */
WebSocketDevice.prototype.getBase64Image = function(img) {
    // Create an empty canvas element
    var canvas = document.createElement("canvas");
    canvas.width = img.width;
    canvas.height = img.height;

    // Copy the image contents to the canvas
    var ctx = canvas.getContext("2d");
    ctx.drawImage(img, 0, 0);

    // Get the data-URL formatted image
    // Firefox supports PNG and JPEG. You could check img.src to
    // guess the original format, but be aware the using "image/jpg"
    // will re-encode the image.
    var dataURL = canvas.toDataURL("image/png");

    return dataURL.replace(/^data:image\/(png|jpg);base64,/, "");
};

//
// Expose the module.
//
if ('undefined' !== typeof module) {
    module.exports = WebSocketDevice;
}