CORE-POS/IS4C

View on GitHub
pos/is4c-nf/scale-drivers/drivers/NewMagellan/SPH_Datacap_Gen2.cs

Summary

Maintainability
F
1 wk
Test Coverage
/*******************************************************************************

    Copyright 2019 Whole Foods Co-op

    This file is part of IT CORE.

    IT CORE is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    IT CORE is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    in the file license.txt along with IT CORE; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

*********************************************************************************/

/*
  This is functionally pretty similar to SPH_Datacap_EMVX. The main read loop
  listens for HTTP requests, runs the request through the appropriate ActiveX
  control, and relays back a HTTP response. There are two significant differences:

  1) Rather than implementing its own HTTP server it uses CHttp which in turn
     uses System.Net.HttpListener and friends. There's nothing necessarily wrong
     with the implementation in SPH_Datacap_EMVX but the standard library one
     is probably more robust.

  2) All ActiveX calls are kept in the same thread (DeviceLoop). Using the same
     control from multiple threads may lead to some instability. Other threads
     that need to interact with the ActiveX controls need to push messages onto
     the work_queue and then monitor http_message for a response.

*/

using System;
using System.IO;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using System.Xml;
using System.Drawing;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Concurrent;
using CustomForms;
using BitmapBPP;
using DSIEMVXLib;
using AxDSIEMVXLib;
using DSIPDCXLib;
using AxDSIPDCXLib;
using ComPort;
using CHttp;

namespace SPH {

public interface IMessage
{
    string type();
    string body();
    string from();
}

public class SimpleMessage : IMessage
{
    private string msg = "";
    public SimpleMessage(string s) {
        this.msg = s;
    }

    public string type() {
        return "simple";
    }

    public string body() {
        return this.msg;
    }

    public string from() {
        return "udp";
    }
}

public class EMVMessage : IMessage
{
    private string msg = "";
    private string f = "";
    public EMVMessage(string s, string f) {
        this.msg = s;
        this.f = f;
    }

    public string type() {
        return "emv";
    }

    public string body() {
        return this.msg;
    }

    public string from() {
        return this.f;
    }
}

public class PDCMessage : IMessage
{
    private string msg = "";
    private string f = "";
    public PDCMessage(string s, string f) {
        this.msg = s;
        this.f = f;
    }

    public string type() {
        return "pdc";
    }

    public string body() {
        return this.msg;
    }

    public string from() {
        return this.f;
    }
}

public class SPH_Datacap_Gen2 : SerialPortHandler 
{
    private DsiEMVX emv_ax_control = null;
    private DsiPDCX pdc_ax_control = null;
    private string device_identifier = null;
    private string com_port = "0";
    protected string server_list = "x1.mercurypay.com;x2.backuppay.com";
    protected int LISTEN_PORT = 8999; // acting as a Datacap stand-in
    protected short CONNECT_TIMEOUT = 60;
    protected string sequence_no = null;
    private RBA_Stub rba = null;
    private string xml_log = null;
    private bool enable_xml_log = false;
    private bool pdc_active;
    private Object pdcLock = new Object();
    private bool emv_reset;
    private bool always_reset = false;
    private bool emv_active;
    private Object emvLock = new Object();
    private string terminalID = "";
    private BlockingCollection<IMessage> work_queue = null;
    private BlockingCollection<string> http_mailbox = null;
    private int device_thread_id;

    public SPH_Datacap_Gen2(string p) : base(p)
    { 
        device_identifier=p;
        if (p.Contains(":")) {
            string[] parts = p.Split(new char[]{':'}, 2);
            device_identifier = parts[0];
            com_port = parts[1];
        }

        string my_location = AppDomain.CurrentDomain.BaseDirectory;
        char sep = Path.DirectorySeparatorChar;
        xml_log = my_location + sep + "log.xml";
        pdc_active = false;
        emv_active = false;
        emv_reset = true;

        if (device_identifier == "INGENICOISC250_MERCURY_E2E") {
            rba = new RBA_Stub("COM"+com_port);
            rba.SetEMV(RbaButtons.EMV);
        }

        this.work_queue = new BlockingCollection<IMessage>();
        this.http_mailbox = new BlockingCollection<string>();
    }

    /**
      Initialize EMVX control with servers
      and response timeout
    */
    protected bool ReInitDevice()
    {
        if (pdc_ax_control == null) {
            pdc_ax_control = new DsiPDCX();
            pdc_ax_control.ServerIPConfig(server_list, 0);
            pdc_ax_control.SetResponseTimeout(CONNECT_TIMEOUT);
            InitPDCX();
        }
        lock (pdcLock) {
            if (pdc_active) {
                Console.WriteLine("Reset PDC");
                pdc_ax_control.CancelRequest();
                pdc_active = false;
            }
        }
        /*
        lock (emvLock) {
            if (emv_active) {
                try {
                    Console.WriteLine("Reset EMV");
                    emv_ax_control.CancelRequest();
                    emv_active = false;
                } catch (Exception) {
                    // I assume this will through if either the ActiveX DLL
                    // was generated against an older OCX that doesn't have
                    // this method or if the DLL has the method but the
                    // OCX does not. OCX v1.22+ should have the method.
                }
            }
        }
        */

        if (emv_ax_control == null) {
            emv_ax_control = new DsiEMVX();
        }
        FlaggedReset();

        if (rba != null) {
            rba.SetParent(this.parent);
            rba.SetVerbose(this.verbose_mode);
            try {
                rba.stubStart();
            } catch (Exception) {}
        }

        return true;
    }

    /**
      Confine all interactions with ActiveX controls to this thread
      Other threads access this thread by adding IMessages to the work_queue
      and should monitor the http_mailbox for responses
    */
    public void DeviceLoop()
    {
        Console.WriteLine("Starting device thread: " + Thread.CurrentThread.ManagedThreadId);
        this.ReInitDevice();
        while (true) {
            var msg = this.work_queue.Take();
            if (msg.type() == "simple") {
                this.ProcessUdpMessage(msg.body());
            } else if (msg.type() == "emv") {
                string result = emv_ax_control.ProcessTransaction(msg.body());
                if (msg.from() == "http") {
                    this.http_mailbox.Add(result);
                }
            } else if (msg.type() == "pdc") {
                string result = pdc_ax_control.ProcessTransaction(msg.body(), 1, null, null);
                if (msg.from() == "http") {
                    this.http_mailbox.Add(result);
                }
            }
        }
    }

    /**
      Driver listens over TCP for incoming HTTP data. Driver
      is providing a web-service style endpoint so POS behavior
      does not have to change. Rather than POSTing information to
      a remote processor it POSTs information to the driver.

      Driver strips off headers, feeds XML into the dsiEMVX control,
      then sends the response back to the client.
    */
    public override void Read()
    { 
        var deviceThread = new Thread(new ThreadStart(this.DeviceLoop));
        deviceThread.Start();
        this.device_thread_id = deviceThread.ManagedThreadId;
        Console.WriteLine("Set device thread: " + this.device_thread_id);
        Server http = new Server();
        http.SetPort(LISTEN_PORT);
        while (SPH_Running) {
            string result = "";
            string keyVal = "key";
            bool saveResult = false;
            try {
                var pair = http.GetNext();

                if (rba != null) {
                    rba.stubStop();
                }

                string message = pair.body;
                message = message.Trim(new char[]{'"'});
                try {
                    XmlDocument request = new XmlDocument();
                    request.LoadXml(message);
                    keyVal = request.SelectSingleNode("TStream/Transaction/InvoiceNo").InnerXml;
                } catch (Exception) {
                    Console.WriteLine("Error parsing from\n" + message);
                }
                // Send EMV messages to EMVX, others
                // to PDCX
                if (message.Contains("EMV")) {
                    result = ProcessEMV(message, true);
                    saveResult = true;
                } else if (message.Contains("termSig")) {
                    FlaggedReset();
                    result = GetSignature(true);
                } else if (message.Length > 0) {
                    result = ProcessPDC(message);
                    saveResult = true;
                }
                http.Respond(pair.response, result);
            } catch (Exception ex) {
                this.LogMessage(ex.ToString());
                try {
                    if (saveResult && result.Length > 0) {
                        parent.SqlLog(keyVal, result);
                    }
                } catch (Exception) {
                    this.LogMessage(keyVal + ": " + result);
                }
            }
        }
    }

    /**
      Pull HTTP body out of string. Simply looking
      for blank line between headers and body
    */
    protected string GetHttpBody(string http_request)
    {
        StringReader sr = new StringReader(http_request);
        string line;
        string ret = "";
        bool headers_over = false;
        while ((line = sr.ReadLine()) != null) {
            if (!headers_over && line == "") {
                headers_over = true;
            } else if (headers_over) {
                ret += line;
            }
        }

        return ret;
    }

    /**
      Add simple HTTP headers to content string
    */
    protected string WrapHttpResponse(string http_response)
    {
        string headers = "HTTP/1.0 200 OK\r\n"
            + "Connection: close\r\n"
            + "Content-Type: text/xml\r\n"
            + "Content-Length: " + http_response.Length + "\r\n" 
            + "Access-Control-Allow-Origin: http://localhost\r\n"
            + "\r\n"; 
        
        return headers + http_response;
    }

    public override void HandleMsg(string msg)
    {
        this.work_queue.Add(new SimpleMessage(msg));
    }

    private void ProcessUdpMessage(string msg)
    { 
        // optional predicate for "termSig" message
        // predicate string is displayed on sig capture screen
        if (msg.Length > 7 && msg.Substring(0, 7) == "termSig") {
            //sig_message = msg.Substring(7);
            msg = "termSig";
        }
        string question = "";
        if (msg.Length > 9 && msg.Substring(0, 9) == "termYesNo") {
            question = msg.Substring(9);
            msg = "termYesNo";
        }
        if (msg.Length > 10 && msg.Substring(0, 10) == "screenLine") {
            string line = msg.Substring(10);
            msg = "IGNORE";
            if (rba != null) {
                rba.addScreenMessage(line);
            }
        }
        switch(msg) {
            case "termReset":
            case "termReboot":
                if (rba != null) {
                    rba.stubStop();
                }
                ReInitDevice();
                break;
            case "termManual":
                break;
            case "termApproved":
                FlaggedReset();
                if (rba != null) {
                    rba.showMessage("Approved");
                }
                break;
            case "termDeclined":
                if (rba != null) {
                    rba.showMessage("Declined");
                }
                break;
            case "termError":
                if (rba != null) {
                    rba.showMessage("Error");
                }
                break;
            case "termSig":
                if (rba != null) {
                    rba.stubStop();
                }
                FlaggedReset();
                GetSignature();
                break;
            case "termYesNo":
                if (rba != null) {
                    rba.stubStop();
                }
                FlaggedReset();
                GetYesNo(question);
                break;
            case "termGetType":
                break;
            case "termGetTypeWithFS":
                break;
            case "termGetPin":
                break;
            case "termWait":
                break;
            case "termFindPort":
                var new_port = this.PortSearch(this.device_identifier);
                if (new_port != "" && new_port != this.com_port && new_port.All(char.IsNumber)) {
                    this.com_port = new_port;
                }
                break;
        }
    }

    public override void SetConfig(string k, string v)
    {
        if (k == "disableRBA" && v == "true") {
            try {
                if (this.rba != null) {
                    rba.stubStop();
                }
            } catch (Exception) {}
            this.rba = null;
        } else if (k == "disableButtons" && v == "true") {
            this.rba.SetEMV(RbaButtons.None);
        } else if (k == "logXML" && v == "true") {
            this.enable_xml_log = true;
        }
    }

    /**
      Supported options:
        -- Global Options --
        * alwaysReset [boolean] default false
            Issue a PadReset command following a transaction. This will make
            the terminal beep until the customer removes their card. Control
            will not be returned to the cashier until the card is removed or
            the reset command times out.
        * logErrors [boolean] default false
            Write error information to the same debug_lane.log file as PHP.
            Errors are logged regardless of whether the verbose switch (-v) 
            is used but not all verbose output is treated as an error & logged
        * logXML [boolean] default false
            Log XML requests & responses to "xml.log" in the current directory.

        -- Ingencio Specific Options --
        * disableRBA [boolean] default false
            Stops all direct communication with Ingenico terminal.
            Driver will solely utilize Datacap functionality
        * disableButtons [boolean] default false
            Does not display payment type or cashback selection buttons.
            RBA commands can still be used to display static text
            Irrelevant if disableRBA is true
        * buttons [string] default EMV
            Change labeling of the buttons. Valid options are "credit"
            and "cashback" currently.
            Irrelevant if disableRBA or disableButtons is true
        * defaultMessage [string] default "Welcome"
            Message displayed onscreen at the start of a transaction
            Irrelevant if disableRBA is true
        * cashback [boolean] default true
            Show cashback selections if payment type debit or ebt cash
            is selected.
            Irrelevant if disableRBA or disableButtons is true
        * servers [string] default "x1.mercurypay.com;x2.backuppay.com"
            Set PDCX server list
    */
    public override void SetConfig(Dictionary<string,string> d)
    {
        if (d.ContainsKey("disableRBA") && d["disableRBA"].ToLower() == "true") {
            try {
                if (this.rba != null) {
                    rba.stubStop();
                }
            } catch (Exception) {}
            this.rba = null;
        }

        if (this.rba != null && d.ContainsKey("disableButtons") && d["disableButtons"].ToLower() == "true") {
            this.rba.SetEMV(RbaButtons.None);
        }

        if (this.rba != null && d.ContainsKey("buttons")) {
            if (d["buttons"].ToLower() == "credit") {
                this.rba.SetEMV(RbaButtons.Credit);
            } else if (d["buttons"].ToLower() == "cashback") {
                this.rba.SetEMV(RbaButtons.Cashback);
            }
        }

        if (this.rba != null && d.ContainsKey("defaultMessage")) {
            this.rba.SetDefaultMessage(d["defaultMessage"]);
        }

        if (d.ContainsKey("alwaysReset") && d["alwaysReset"].ToLower() == "true") {
            this.always_reset = true;
        }

        if (d.ContainsKey("logXML") && d["logXML"].ToLower() == "true") {
            this.enable_xml_log = true;
        }

        if (d.ContainsKey("logErrors") && d["logErrors"].ToLower() == "true") {
            this.enableUnifiedLog();
        }

        if (this.rba != null && d.ContainsKey("cashback") && (d["cashback"].ToLower() == "true" || d["cashback"].ToLower() == "false")) {
            this.rba.SetCashBack(d["cashback"].ToLower() == "true" ? true : false);
        }

        if (d.ContainsKey("servers")) {
            this.server_list = d["servers"];
        }

        if (d.ContainsKey("terminalID")) {
            this.terminalID = d["terminalID"];
        }
    }

    /**
      Process XML transaction using dsiPDCX
      @param xml the request body
      @param autoReset true if the request requires a reset, false if the request IS a reset
    */
    protected string ProcessEMV(string xml, bool autoReset)
    {
        /* 
           Substitute values into the XML request
           This is so the driver can handle any change
           in which hardware device is connected as well
           as so tracking SequenceNo values is not POS'
           problem.
        */
        xml = xml.Replace("{{SequenceNo}}", SequenceNo());
        if (IsCanadianDeviceType(this.device_identifier)) {
            // tag name is different in this case;
            // replace placeholder then the open/close tags
            xml = xml.Replace("{{SecureDevice}}", this.device_identifier);
            xml = xml.Replace("SecureDevice", "PadType");
        } else {
            xml = xml.Replace("{{SecureDevice}}", SecureDeviceToEmvType(this.device_identifier));
        }
        xml = xml.Replace("{{ComPort}}", com_port);
        if (this.terminalID.Length > 0) {
            xml = xml.Replace("{{TerminalID}}", this.terminalID);
        }

        try {
            /**
              Extract HostOrIP field and split it on commas
              to allow multiple IPs
            */
            XmlDocument request = new XmlDocument();
            request.LoadXml(xml);
            var IPs = request.SelectSingleNode("TStream/Transaction/HostOrIP").InnerXml.Split(new Char[]{','}, StringSplitOptions.RemoveEmptyEntries);
            string result = "";
            foreach (string IP in IPs) {
                // try request with an IP

                // If this is NOT a pad reset request, check the emv_reset
                // flag to see if a reset is needed. If so, execute one
                // and update the flag
                if (autoReset) {
                    FlaggedReset();
                }

                lock(emvLock) {
                    emv_active = true;
                }

                request.SelectSingleNode("TStream/Transaction/HostOrIP").InnerXml = IP;
                /*
                   To avoid a big refactor check thread IDs to see if we're already
                   in the DeviceLoop thread. Some messages processed in that thread
                   might trigger additional calls to ProcessEMV or ProcessPDC and 
                   recursive adding jobs to the work queue will deadlock
                */
                if (Thread.CurrentThread.ManagedThreadId != this.device_thread_id) {
                    this.work_queue.Add(new EMVMessage(request.OuterXml, "http"));
                    result = this.http_mailbox.Take();
                } else {
                    result = emv_ax_control.ProcessTransaction(request.OuterXml);
                }

                lock(emvLock) {
                    emv_active = false;
                    // if this is not a reset command, set the reset needed flag
                    if (autoReset) {
                        emv_reset = true;
                    }
                }

                if (enable_xml_log) {
                    using (StreamWriter sw = new StreamWriter(xml_log, true)) {
                        sw.WriteLine(DateTime.Now.ToString() + " (send emv): " + request.OuterXml);
                        sw.WriteLine(DateTime.Now.ToString() + " (recv emv): " + result);
                    }
                }
                XmlDocument doc = new XmlDocument();
                try {
                    doc.LoadXml(result);
                    // track SequenceNo values in responses
                    XmlNode sequence = doc.SelectSingleNode("RStream/CmdResponse/SequenceNo");
                    sequence_no = sequence.InnerXml;
                    XmlNode return_code = doc.SelectSingleNode("RStream/CmdResponse/DSIXReturnCode");
                    XmlNode origin = doc.SelectSingleNode("RStream/CmdResponse/ResponseOrigin");
                    /**
                      On anything that is not a local connectivity failure, exit the
                      loop and return the result without trying any further IPs.
                    */
                    if (origin.InnerXml != "Client" || return_code.InnerXml != "003006") {
                        break;
                    }
                } catch (Exception ex) {
                    // response was invalid xml
                    this.LogMessage(ex.ToString());
                    // status is unclear so do not attempt 
                    // another transaction
                    break;
                }
            }

            if (autoReset && this.always_reset) {
                FlaggedReset();
            }

            return result;

        } catch (Exception ex) {
            // request was invalid xml
            this.LogMessage(ex.ToString());
        }

        return "";
    }

    /**
      Process XML transaction using dsiPDCX
    */
    protected string ProcessPDC(string xml)
    {
        lock (pdcLock) {
            pdc_active = true;
        }
        string ret = "";
        try {
            xml = xml.Replace("{{SequenceNo}}", SequenceNo());
            xml = xml.Replace("{{SecureDevice}}", this.device_identifier);
            xml = xml.Replace("{{ComPort}}", com_port);
            if (this.terminalID.Length > 0) {
                xml = xml.Replace("{{TerminalID}}", this.terminalID);
            }

            /*
               To avoid a big refactor check thread IDs to see if we're already
               in the DeviceLoop thread. Some messages processed in that thread
               might trigger additional calls to ProcessEMV or ProcessPDC and 
               recursive adding jobs to the work queue will deadlock
            */
            if (Thread.CurrentThread.ManagedThreadId != this.device_thread_id) {
                this.work_queue.Add(new PDCMessage(xml, "http"));
                ret = this.http_mailbox.Take();
            } else {
                ret = pdc_ax_control.ProcessTransaction(xml, 1, null, null);
            }
            if (enable_xml_log) {
                using (StreamWriter sw = new StreamWriter(xml_log, true)) {
                    sw.WriteLine(DateTime.Now.ToString() + " (send pdc): " + xml);
                    sw.WriteLine(DateTime.Now.ToString() + " (recv pdc): " + ret);
                }
            }
        } catch (Exception ex) {
            this.LogMessage(ex.ToString());
        }
        lock (pdcLock) {
            pdc_active = false;
        }

        return ret;
    }

    /**
      Get the current sequence number OR the default
    */
    protected string SequenceNo()
    {
        return sequence_no != null ? sequence_no : "0010010010";
    }

    /**
      PDCX initialize device
    */
    protected string InitPDCX()
    {
        string xml="<?xml version=\"1.0\"?>"
            + "<TStream>"
            + "<Admin>"
            + "<MerchantID>MerchantID</MerchantID>"
            + "<TranCode>SecureDeviceInit</TranCode>"
            + "<TranType>Setup</TranType>"
            + "<SecureDevice>"+ this.device_identifier + "</SecureDevice>"
            + "<ComPort>" + this.com_port + "</ComPort>"
            + "<PadType>" + SecureDeviceToPadType(device_identifier) + "</PadType>"
            + "</Admin>"
            + "</TStream>";
        
        return ProcessPDC(xml);
    }

    /**
      Set a lock, check if the reset flag is active.
      If so, issue a reset and clear the flag then
      release the lock
    */
    protected string FlaggedReset()
    {
        string ret = "";
        lock(emvLock) {
            if (emv_reset) {
                ret  = PadReset();
                emv_reset = false;
            }
        }

        return ret;
    }

    /**
      EMVX reset device for next transaction
    */
    protected string PadReset()
    {
        string xml="<?xml version=\"1.0\"?>"
            + "<TStream>"
            + "<Transaction>"
            + "<HostOrIP>127.0.0.1</HostOrIP>"
            + "<MerchantID>MerchantID</MerchantID>"
            + (this.terminalID.Length > 0 ? "<TerminalID>" + this.terminalID + "</TerminalID>" : "")
            + "<TranCode>EMVPadReset</TranCode>"
            + "<SecureDevice>" + SecureDeviceToEmvType(this.device_identifier) + "</SecureDevice>"
            + "<ComPort>" + this.com_port + "</ComPort>"
            + "<SequenceNo>" + SequenceNo() + "</SequenceNo>"
            + "</Transaction>"
            + "</TStream>";
    
        return ProcessEMV(xml, false);
    }

    /**
      PDCX method to get signature from device
    */
    protected string GetSignature(bool udp=true)
    {
        string xml="<?xml version=\"1.0\"?>"
            + "<TStream>"
            + "<Transaction>"
            + "<MerchantID>MerchantID</MerchantID>"
            + "<TranCode>GetSignature</TranCode>"
            + "<SecureDevice>"+ this.device_identifier + "</SecureDevice>"
            + "<ComPort>" + this.com_port + "</ComPort>"
            + "<Account>"
            + "<AcctNo>SecureDevice</AcctNo>"
            + "</Account>"
            + "</Transaction>"
            + "</TStream>";
        string result = ProcessPDC(xml);
        XmlDocument doc = new XmlDocument();
        try {
            doc.LoadXml(result);
            XmlNode status = doc.SelectSingleNode("RStream/CmdResponse/CmdStatus");
            if (status.InnerText != "Success") {
                return null;
            }
            string sigdata = doc.SelectSingleNode("RStream/Signature").InnerText;
            List<Point> points = SigDataToPoints(sigdata);

            string my_location = AppDomain.CurrentDomain.BaseDirectory;
            char sep = Path.DirectorySeparatorChar;
            string ticks = System.Guid.NewGuid().ToString();
            string filename = my_location + sep + "ss-output"+ sep + "tmp" + sep + ticks + ".bmp";
            BitmapBPP.Signature sig = new BitmapBPP.Signature(filename, points);
            if (udp) {
                parent.MsgSend("TERMBMP" + ticks + ".bmp");
            } else {
                return "<img>" + ticks + ".bmp</img>";
            }
        } catch (Exception ex) {
            this.LogMessage(ex.ToString());
        }

        return "<err>Error collecting signature</err>";
    }

    protected string GetYesNo(string question)
    {
        string xml="<?xml version=\"1.0\"?>"
            + "<TStream>"
            + "<Transaction>"
            + "<HostOrIP>127.0.0.1</HostOrIP>"
            + "<MerchantID>MerchantID</MerchantID>"
            + (this.terminalID.Length > 0 ? "<TerminalID>" + this.terminalID + "</TerminalID>" : "")
            + "<TranCode>GetAnswer</TranCode>"
            + "<SecureDevice>" + SecureDeviceToEmvType(this.device_identifier) + "</SecureDevice>"
            + "<ComPort>" + this.com_port + "</ComPort>"
            + "<SequenceNo>" + SequenceNo() + "</SequenceNo>"
            + (question.Length > 0 ? "<Question>" + question + "</Question>" : "")
            + "</Transaction>"
            + "</TStream>";
        string result = ProcessEMV(xml, false);
        XmlDocument doc = new XmlDocument();
        try {
            doc.LoadXml(result);
            XmlNode status = doc.SelectSingleNode("RStream/CmdResponse/CmdStatus");
            if (status.InnerText == "Success") {
                this.PadReset();
                parent.MsgSend("termYes");
                return "";
            } else if (status.InnerText == "Error") {
                XmlNode textResp = doc.SelectSingleNode("RStream/CmdResponse/TextResponse");
                if (textResp.InnerText.Contains("'No' Pressed")) {
                    this.PadReset();
                    parent.MsgSend("termNo");
                    return "";
                }
            }
        } catch (Exception ex) {
            this.LogMessage(ex.ToString());
        }

        return "<err>Error collecting answer</err>";
    }

    protected string PortSearch(string device)
    {
        switch (device) {
            case "VX805XPI":
            case "VX805XPI_MERCURY_E2E":
                return ComPortUtility.FindComPort("Verifone");
            case "INGENICOISC250":
            case "INGENICOISC250_MERCURY_E2E":
            case "INGENICOLANE8000_MERCURY":
            case "INGENICOISC250_RAPIDCONNECT_E2E":
                return ComPortUtility.FindComPort("Ingenico");
            default:
                return "";
        }
    }

    /**
      Translate securedevice strings to padtype strings
    */
    protected string SecureDeviceToPadType(string device)
    {
        switch (device) {
            case "VX805XPI":
            case "VX805XPI_MERCURY_E2E":
                return "VX805";
            case "INGENICOISC250":
            case "INGENICOISC250_MERCURY_E2E":
            case "INGENICOISC250_RAPIDCONNECT_E2E":
                return "ISC250";
            case "INGENICOIPP320":
                return "IPP320";
            case "INGENICOLANE8000_MERCURY":
                return "LANE8000";
            default:
                return device;
        }
    }

    /**
      Translate pdc securedevice strings to emv securedevice strings
    */
    protected string SecureDeviceToEmvType(string device)
    {
        switch (device) {
            case "VX805XPI":
                return "EMV_VX805_RAPIDCONNECT";
            case "VX805XPI_MERCURY_E2E":
                return "EMV_VX805_MERCURY";
            case "INGENICOISC250_MERCURY_E2E":
                return "EMV_ISC250_MERCURY";
            case "INGENICOISC250_RAPIDCONNECT_E2E":
                return "EMV_ISC250_RAPIDCONNECT_E2E";
            case "INGENICOIPP320":
                return "EMV_IPP320_RAPIDCONNECT";
            case "INGENICOLANE8000_MERCURY":
                return "EMV_LANE8000_MERCURY";
            default:
                return "EMV_" + device;
        }

    }

    protected bool IsCanadianDeviceType(string device)
    {
        switch (device) {
            case "Paymentech1":
            case "Global1":
            case "Moneris1":
                return true;
            default:
                return false;
        }
    }

    protected List<Point> SigDataToPoints(string data)
    {
        char[] comma = new char[]{','};
        char[] colon = new char[]{':'};
        var pairs = from pair in data.Split(colon) 
            select pair.Split(comma);
        var points = from pair in pairs 
            where pair.Length == 2
            select new Point(CoordsToInt(pair[0]), CoordsToInt(pair[1]));

        return points.ToList();
    }

    protected int CoordsToInt(string coord)
    {
        if (coord == "#") {
            return 0;
        } else {
            return Int32.Parse(coord);
        }
    }
}

}