如何与3D打印机正确通信
How to correctly communicate with 3D Printer
我必须编写一个 java 程序,通过网络接收 G 代码命令并通过串行通信将它们发送到 3D 打印机。原则上一切似乎都没有问题,只要打印机需要超过 300ms 来执行一条命令。如果执行时间短于此,则打印机接收下一条命令需要花费太多时间,导致命令执行之间存在延迟(打印机喷嘴静止约 100-200 毫秒)。这可能成为 3d 打印中的一个问题,所以我必须消除这种延迟。
作为比较:Repetier Host 或 Cura 等软件可以通过 seial 发送相同的命令,而命令执行之间没有任何延迟,因此必须以某种方式实现。
我使用 jSerialComm 库进行串行通信。
这是向打印机发送命令的线程:
@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;
}
这是 printer.serialWrite:("Inspired" 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
是 class Printer
的对象,它实现了 com.fazecast.jSerialComm.SerialPortDataListener
打印机的相关功能
@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
被声明为 volatile
我还尝试在另一个线程中阻止 jserialcomm 的功能,结果相同。
我的瓶颈在哪里?我的代码是否存在瓶颈,或者 jserialcomm 是否会产生过多的开销?
对于没有 3d 打印经验的人:
当打印机接收到有效命令时,它将将该命令放入内部缓冲区以最大限度地减少延迟。只要内部缓冲区中有空闲 space,它就会回复 ok
。当缓冲区已满时,ok
会延迟,直到再次有空闲 space。
所以基本上你只需要发送一个命令,等待确定,立即发送另一个。
@Override
public void serialEvent(SerialPortEvent serialPortEvent) {
byte[] newData = new byte[comPort.bytesAvailable()];
int numRead = comPort.readBytes(newData, newData.length);
handleData(new String(newData));
}
这部分有问题,事件可能在 读取整行 之前触发,因此可能只收到一半 ok
。在尝试将其解析为完整消息之前,您需要先缓冲(通过多个事件)并重新组合成消息。
最坏的情况是,这可能导致温度读数完全丢失或 ok
消息被撕成两半。
查看 InputStream example 并将其包裹在 BufferedReader
中以访问 BufferedReader::readLine()
。使用 BufferedReader
,您可以直接在主线程中使用它来轮询并同步处理响应。
try{Thread.sleep(5);} catch(Exception e){}
sleep(1);
你不想睡觉。根据您的系统环境(我强烈假设这不是 x86 上 Windows 上的 运行,而是嵌入式平台上的 Linux),sleep
可以是比预期的要长得多。最多 30 毫秒或 100 毫秒,具体取决于内核配置。
写入前的睡眠首先没有多大意义,您知道串行端口已准备好写入,因为您已经收到 ok
确认接收到先前发送的命令。
使用 BufferedReader
.
时,接收期间的休眠变得毫无意义
comPort.setComPortTimeouts(SerialPort.TIMEOUT_SCANNER, 0, 500);
这实际上是您的问题所在。 SerialPort.TIMEOUT_SCANNER
激活读取等待时间。在接收到第一个字节后,它将至少再等待 100 毫秒以查看它是否会成为消息的一部分。因此,在它看到 ok
之后,它会在 OS 端内部等待 100 毫秒,然后才会认为这就是全部。
您需要 SerialPort.TIMEOUT_READ_SEMI_BLOCKING
以获得低延迟,但如果不进行缓冲,就会出现第一段中预测的问题。
重复设置也会导致又一个问题,因为Serialport::setComPortTimeouts
内部有一个200ms的休眠。每个串行连接设置一次,仅此而已。
查看打印机的说明书(或告诉我们型号)不确定您是否确实需要等待 ok
,因此您可以同时 read/write。有时会有一个硬件流控制为你处理这些东西,有足够大的缓冲区。尝试直接发送命令而不等待 ok
,看看会发生什么。
如果您只想将命令从网络传输到串口,您可以使用现成的解决方案,例如socat。例如 运行 如下:
socat TCP-LISTEN:8888,fork,reuseaddr FILE:/dev/ttyUSB0,b115200,raw
将以 115200 的波特率将来自连接到 8888 端口的客户端的所有字节直接传输到 /dev/ttyUSB0
(反之亦然)。
我必须编写一个 java 程序,通过网络接收 G 代码命令并通过串行通信将它们发送到 3D 打印机。原则上一切似乎都没有问题,只要打印机需要超过 300ms 来执行一条命令。如果执行时间短于此,则打印机接收下一条命令需要花费太多时间,导致命令执行之间存在延迟(打印机喷嘴静止约 100-200 毫秒)。这可能成为 3d 打印中的一个问题,所以我必须消除这种延迟。
作为比较:Repetier Host 或 Cura 等软件可以通过 seial 发送相同的命令,而命令执行之间没有任何延迟,因此必须以某种方式实现。
我使用 jSerialComm 库进行串行通信。
这是向打印机发送命令的线程:
@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;
}
这是 printer.serialWrite:("Inspired" 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
是 class Printer
的对象,它实现了 com.fazecast.jSerialComm.SerialPortDataListener
打印机的相关功能
@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
被声明为 volatile
我还尝试在另一个线程中阻止 jserialcomm 的功能,结果相同。
我的瓶颈在哪里?我的代码是否存在瓶颈,或者 jserialcomm 是否会产生过多的开销?
对于没有 3d 打印经验的人:
当打印机接收到有效命令时,它将将该命令放入内部缓冲区以最大限度地减少延迟。只要内部缓冲区中有空闲 space,它就会回复 ok
。当缓冲区已满时,ok
会延迟,直到再次有空闲 space。
所以基本上你只需要发送一个命令,等待确定,立即发送另一个。
@Override
public void serialEvent(SerialPortEvent serialPortEvent) {
byte[] newData = new byte[comPort.bytesAvailable()];
int numRead = comPort.readBytes(newData, newData.length);
handleData(new String(newData));
}
这部分有问题,事件可能在 读取整行 之前触发,因此可能只收到一半 ok
。在尝试将其解析为完整消息之前,您需要先缓冲(通过多个事件)并重新组合成消息。
最坏的情况是,这可能导致温度读数完全丢失或 ok
消息被撕成两半。
查看 InputStream example 并将其包裹在 BufferedReader
中以访问 BufferedReader::readLine()
。使用 BufferedReader
,您可以直接在主线程中使用它来轮询并同步处理响应。
try{Thread.sleep(5);} catch(Exception e){}
sleep(1);
你不想睡觉。根据您的系统环境(我强烈假设这不是 x86 上 Windows 上的 运行,而是嵌入式平台上的 Linux),sleep
可以是比预期的要长得多。最多 30 毫秒或 100 毫秒,具体取决于内核配置。
写入前的睡眠首先没有多大意义,您知道串行端口已准备好写入,因为您已经收到 ok
确认接收到先前发送的命令。
使用 BufferedReader
.
comPort.setComPortTimeouts(SerialPort.TIMEOUT_SCANNER, 0, 500);
这实际上是您的问题所在。 SerialPort.TIMEOUT_SCANNER
激活读取等待时间。在接收到第一个字节后,它将至少再等待 100 毫秒以查看它是否会成为消息的一部分。因此,在它看到 ok
之后,它会在 OS 端内部等待 100 毫秒,然后才会认为这就是全部。
您需要 SerialPort.TIMEOUT_READ_SEMI_BLOCKING
以获得低延迟,但如果不进行缓冲,就会出现第一段中预测的问题。
重复设置也会导致又一个问题,因为Serialport::setComPortTimeouts
内部有一个200ms的休眠。每个串行连接设置一次,仅此而已。
查看打印机的说明书(或告诉我们型号)不确定您是否确实需要等待 ok
,因此您可以同时 read/write。有时会有一个硬件流控制为你处理这些东西,有足够大的缓冲区。尝试直接发送命令而不等待 ok
,看看会发生什么。
如果您只想将命令从网络传输到串口,您可以使用现成的解决方案,例如socat。例如 运行 如下:
socat TCP-LISTEN:8888,fork,reuseaddr FILE:/dev/ttyUSB0,b115200,raw
将以 115200 的波特率将来自连接到 8888 端口的客户端的所有字节直接传输到 /dev/ttyUSB0
(反之亦然)。