javaserial-portserial-communication3d-printing

How to correctly communicate with 3D Printer


I have to write a java program that receives G-Code commands via network and sends them to a 3D printer via serial communication. In principle everything seems to be okay, as long as the printer needs more than 300ms to execute a command. If execution time is shorter than that, it takes too much time for the printer to receive the next command and that results in a delay between command execution (printer nozzle standing still for about 100-200ms). This can become a problem in 3d printing so i have to eliminate that delay.

For comparison: Software like Repetier Host or Cura can send the same commands via seial without any delay between command execution, so it has to be possible somehow.

I use jSerialComm library for serial communication.

This is the Thread that sends commands to the printer:

@Override
public void run() {
if(printer == null) return;
    log("Printer Thread started!");
    //wait just in case
    Main.sleep(3000);

    long last = 0;
    while(true) {

        String cmd = printer.cmdQueue.poll();
        if (cmd != null && !cmd.equals("") && !cmd.equals("\n")) {
            log(cmd+" last: "+(System.currentTimeMillis()-last)+"ms");
            last = System.currentTimeMillis();
            send(cmd  + "\n", 0);
        }

    }
}

private void send(String cmd, int timeout) {
    printer.serialWrite(cmd);
    waitForBuffer(timeout);
}

private void waitForBuffer(int timeout) {
    if(!blockForOK(timeout))
        log("OK Timeout ("+timeout+"ms)");
}

public boolean blockForOK(int timeoutMillis) {
    long millis = System.currentTimeMillis();
    while(!printer.bufferAvailable) {
        if(timeoutMillis != 0)
            if(millis + timeoutMillis < System.currentTimeMillis()) return false;
        try {
            sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    printer.bufferAvailable = false;
    return true;
}

this is printer.serialWrite: ("Inspired" by Arduino Java Lib)

public void serialWrite(String s){
    comPort.setComPortTimeouts(SerialPort.TIMEOUT_SCANNER, 0, 500);
    try{Thread.sleep(5);} catch(Exception e){}

    PrintWriter pout = new PrintWriter(comPort.getOutputStream());
    pout.print(s);
    pout.flush();

}

printer is an Object of class Printer which implements com.fazecast.jSerialComm.SerialPortDataListener

relevant functions of Printer

@Override
public int getListeningEvents() {
    return SerialPort.LISTENING_EVENT_DATA_AVAILABLE;

}

@Override
public void serialEvent(SerialPortEvent serialPortEvent) {
    byte[] newData = new byte[comPort.bytesAvailable()];
    int numRead = comPort.readBytes(newData, newData.length);
    handleData(new String(newData));
}

private void handleData(String line) {
    //log("RX: "+line);
    if(line.contains("ok")) {
        bufferAvailable = true;
    }
    if(line.contains("T:")) {
        printerThread.printer.temperature[0] = Utils.readFloat(line.substring(line.indexOf("T:")+2));
    }
    if(line.contains("T0:")) {
        printerThread.printer.temperature[0] = Utils.readFloat(line.substring(line.indexOf("T0:")+3));
    }
    if(line.contains("T1:")) {
        printerThread.printer.temperature[1] = Utils.readFloat(line.substring(line.indexOf("T1:")+3));
    }
    if(line.contains("T2:")) {
        printerThread.printer.temperature[2] = Utils.readFloat(line.substring(line.indexOf("T2:")+3));
    }
}

Printer.bufferAvailable is declared volatile I also tried blocking functions of jserialcomm in another thread, same result. Where is my bottleneck? Is there a bottleneck in my code at all or does jserialcomm produce too much overhead?

For those who do not have experience in 3d-printing: When the printer receives a valid command, it will put that command into an internal buffer to minimize delay. As long as there is free space in the internal buffer it replies with ok. When the buffer is full, the ok is delayed until there is free space again. So basicly you just have to send a command, wait for the ok, send another one immediately.


Solution

  • @Override
    public void serialEvent(SerialPortEvent serialPortEvent) {
        byte[] newData = new byte[comPort.bytesAvailable()];
        int numRead = comPort.readBytes(newData, newData.length);
        handleData(new String(newData));
    }
    

    This part is problematic, the event may have been triggered before a full line was read, so potentially only half an ok has been received yet. You need to buffer (over multiple events) and reassamble into messages first before attempting to parse this as full messages.

    Worst case, this may have resulted in entirely loosing temperature readings or ok messages as they have been ripped in half.

    See the InputStream example and wrap it in a BufferedReader to get access to BufferedReader::readLine(). With the BufferedReader in place, you can that just use that to poll directly in the main thread and process the response synchronously.


    try{Thread.sleep(5);} catch(Exception e){}
    sleep(1);
    

    You don't want to sleep. Depending on your system environment (and I strongly assume that this isn't running on Windows on x86, but rather Linux on an embedded platform), a sleep can be much longer than anticipated. Up to 30ms or 100ms, depending on the Kernel configuration.

    The sleep before write doesn't make much sense in the first place, you know that the serial port is ready to write as you already had received an ok confirming reception of the previously sent command.

    The sleep during receive becomes pointless when using the BufferedReader.


    comPort.setComPortTimeouts(SerialPort.TIMEOUT_SCANNER, 0, 500);
    

    And this is actually causing your problems. SerialPort.TIMEOUT_SCANNER activates a wait period on read. After receiving the first byte it will wait at least for another 100ms to see if it will become part of a message. So after it has seen the ok it then waits 100ms internally on the OS side before it assumes that this was all there is.

    You need SerialPort.TIMEOUT_READ_SEMI_BLOCKING for low latency, but then the problem predicted in the first paragraph will occur unless buffered.

    Setting repeatedly also causes yet another problem, because there is a 200ms sleep in Serialport::setComPortTimeouts internally. Set it per serial connection once, no more than that.