How to script a serial port that talks back

[Original blog post by David Beauchamp - 11/30/2017]

In my previous article I covered the basics of serial port communication in xTuple ERP, sending data to a device and pretending it cannot send data back. While this may work well for LCD's, many devices will expect you to listen when they talk. Building on our previous work, we will create a screen to talk to a magnetic stripe reader/writer such as the MSR series devices.

Previously we hardcoded the serial port name into the script, however the xTuple scripting environment provides a way to get a list of serial ports on the system and we can harness this ability:

var ports = QSerialPortInfo.availablePorts();
for (var i = 0; i < ports.length; i += 1) { //iterate over the available ports and add them to our combobox
  _serialPort.append(i, ports[i].portName); 
}

Same with baud rates:

var rates = QSerialPortInfo.standardBaudRates();
for (var i = 0; i < rates.length; i += 1) {
  _baudRate.append(i, rates[i]);
}

This time around our device has more than a few things it commands it can accept, each one of them sending a different command over the wire. In order to make this easier for the end user, we will build an object containing the list of commands mapping to their JavaScript function within our script:

var commands = {
  "0": {name: "Read", func: sReadData},
  "1": {name: "Write", func: sWriteData},
  "2": {name: "Erase", func: sEraseCard},
  "3": {name: "Set Hi-Co", func: sSetCoercivityLo},
  "4": {name: "Set Lo-Co", func: sSetCoercivityHi},
  "5": {name: "Get Model", func: sGetModel},
  "6": {name: "Get Version", func: sGetVersion},
  "7": {name: "Comm Test", func: sCommTest},
};

An example of a function that requests the version from the device:

function sGetVersion() {
  if (sp.isOpen()) {
    var t = new QByteArray();
    t.append(ESC);
    t.append(0x76);
    sp.write(t);
    _byteCount.value += t.size();
    _output.text = "Waiting for version...";
  }
  else {
    sShowSpError();
  }
}

And now for the fun, once we send this command to the device we need to be ready for it to answer us!

function sOpenSerialPort() {

  if (sp.isOpen()) { return; }

  sp.setPortName(_serialPort.text);
  sp.setBaudRate(_baudRate.text);

  if (!sp.open(QIODevice.ReadWrite)) {
    _portStatus.setStyleSheet(red);
    _portStatus.text = "ERROR";
    QMessageBox.critical(mywindow, "Error", sp.error + "");
    return;
  }
  else {
    _portStatus.setStyleSheet(green);
    _portStatus.text = "OPEN";
    sp.readyRead.connect(sHandleReadyRead);
  }
}

function sHandleReadyRead() {
  bytes.append(sp.readAll());
  while (sp.waitForReadyRead(200)) {
    bytes.append(sp.readAll());
  }

  if (!bytes.isEmpty()) {
    var received = bytes.toString();
    var parsed = parseData(bytes);
    if (parsed.success && parsed.card) {
      _track1.plainText = parsed.card.track1;
      _track2.plainText = parsed.card.track2;
      _track3.plainText = parsed.card.track3;
      _output.text = parsed.message;
    }
    else if (parsed.success) {
      _output.text = parsed.message;
    }
    else {
      _output.text = parsed.message;
    }
    _byteCount.value += bytes.size();
    bytes.clear();
  }
}

handleReadyRead() is our main function, I included the code to open the serial port to show where we make out connection to this signal. Once we have it, we read in the full bytes coming over the wire and parse them out. 

What we do with the data is up to us, in this case I am expecting a 3 track magnetic stripe card and we need to parse the data appropriately. ISO7811 is the standard here and our function handles this.