How to correctly communicate with 3D Printer
Asked Answered
A

2

6

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.

Ancestral answered 8/4, 2018 at 16:40 Comment(5)
"Where is my bottleneck?" -- I don't know Java, but I see a sleep(1) in your blockForOK() routine. That implies that your code is not event driven, but polls (at a one-second rate?), so it's not going to be very responsive. The worst-case scenario is that the wait condition tests true but becomes false right after the test, so that the full delay interval (1000 milliseconds?) elapses before the go-ahead condition is finally recognized. Your average delay is 500 ms to respond?Theis
sleep(1) causes a delay of 1 millisecond, not 1000ms. Also, I have tried event driven code which does not eliminate the delay.Ancestral
Wow had the very same issue for my degree, I have never thought that others will have this problem as well... but for me the event driven solution was working quite well. Could you include your code regarding that?Wenz
No solution but some thoughts. I would not use System.currentTimeMillis() for measurements because it is imprecise - see here. However your timeout is set to 0, so I would not consider this as issue. My guess would be the sleep(1) which puts the thread to sleep for at least 1ms and it depends on the scheduler when the thread becomes active again - see here. Would it be possible to remove the thread at all and add your business logic to handleData()instead of using the volatile boolean?Hoebart
Thanks for your thoughts! What exactly do you mean by business logic? If you mean the part which calls send (at the moment in the ´run´ of ´Printer Thread´), i think that COULD give me more delay because handleData() is only called when there is a command received from the printer. But on the other hand I should really try that.Ancestral
A
4
@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.

Afferent answered 9/5, 2018 at 5:47 Comment(1)
This sounds very plausible. I'll test it asap and give you feedback! (and bounty)Ancestral
V
1

Check the manual of the printer (or tell us the model) not sure you actually need to wait for the ok, and therefore you can read/write concurrently. Some of the time there's a hardware flow control handling this stuff for you, with large enough buffers. Try just send the commands without waiting for ok, see what happens.

If you just want to pipe commands from the network to serial port, you can use ready-made solution like socat. For example running the following:

socat TCP-LISTEN:8888,fork,reuseaddr FILE:/dev/ttyUSB0,b115200,raw

would pipe all bytes coming from clients connected to the 8888 port directly to the /dev/ttyUSB0 at baud rate of 115200 (and vice-versa).

Volnak answered 9/5, 2018 at 11:3 Comment(1)
If I send without any delay it works for the first few commands and then it seems to skip commands. So hardware flowcontrol isn't present here.Ancestral

© 2022 - 2024 — McMap. All rights reserved.