整理&排版 | 嵌入式應(yīng)用研究院
眾所周知,對(duì)3D打印機(jī)感興趣的小伙伴來說,都清楚Cura是3D打印機(jī)的切片軟件,它的UI部分是基于QT來開發(fā)的。而Cura中很多功能其實(shí)是基于插件的形式來開發(fā),其中,用于實(shí)現(xiàn)Cura的USB轉(zhuǎn)串口聯(lián)機(jī)打印的邏輯就是一個(gè)插件,它是使用Python語言來實(shí)現(xiàn)的,具體代碼位于:
https://github.com/Ultimaker/Cura/tree/main/plugins/USBPrinting
而我前陣子參加開放原子基金會(huì)組織的開發(fā)者成長(zhǎng)激勵(lì)活動(dòng)的作品其實(shí)也算是聯(lián)機(jī)打印的一種,只是實(shí)現(xiàn)的方式不同而已罷了:
開發(fā)者成長(zhǎng)激勵(lì)計(jì)劃-基于TencentOS Tiny FDM 3D打印機(jī)云控制系統(tǒng)方案
說到Cura中的USB轉(zhuǎn)串口聯(lián)機(jī)打印,核心邏輯可以梳理下為以下幾點(diǎn):
(1)查找串口設(shè)備列表并獲取對(duì)應(yīng)的打印機(jī)設(shè)備端口號(hào),這部分的代碼是在USBPrinterOutputDeviceManager.py
這個(gè)文件里實(shí)現(xiàn)的。
(2)設(shè)置串口設(shè)備參數(shù)并連接設(shè)備、啟動(dòng)更新線程來處理串口數(shù)據(jù)接收
具體的代碼實(shí)現(xiàn)如下:
def connect(self):
self._firmware_name = None # after each connection ensure that the firmware name is removed
if self._baud_rate is None:
if self._use_auto_detect:
auto_detect_job = AutoDetectBaudJob(self._serial_port)
auto_detect_job.start()
auto_detect_job.finished.connect(self._autoDetectFinished)
return
if self._serial is None:
try:
# 設(shè)置串口參數(shù)
self._serial = Serial(str(self._serial_port), self._baud_rate, timeout=self._timeout, writeTimeout=self._timeout)
except SerialException:
Logger.warning("An exception occurred while trying to create serial connection.")
return
except OSError as e:
Logger.warning("The serial device is suddenly unavailable while trying to create a serial connection: {err}".format(err = str(e)))
return
CuraApplication.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged)
self._onGlobalContainerStackChanged()
self.setConnectionState(ConnectionState.Connected)
# 啟動(dòng)更新線程
self._update_thread.start()
(3)啟動(dòng)更新任務(wù)線程,更新任務(wù)線程的作用是處理以下幾件事情:
以readline()
的方式去接收打印機(jī)回復(fù)的數(shù)據(jù),然后處理數(shù)據(jù),例如接收到了ok或者溫度信息等。
處理接收的數(shù)據(jù),并接著發(fā)下一條Gcode指令,直到?jīng)]有得發(fā)為止。
處理打印過程中發(fā)生的異常事件
發(fā)送M105
獲取溫度命令,這里Cura
是做了一些處理的,發(fā)送該條命令的前提是打印機(jī)不處于忙狀態(tài)并且溫度到了設(shè)定的固件超時(shí)時(shí)間才會(huì)進(jìn)行發(fā)送。Cura的超時(shí)設(shè)置為3s。
具體的代碼實(shí)現(xiàn)如下:
# 線程_update_thread->更新任務(wù)函數(shù)的實(shí)現(xiàn)
def _update(self):
while self._connection_state == ConnectionState.Connected and self._serial is not None:
try:
line = self._serial.readline()
except:
continue
# 獲取固件信息
# 如果是Marlin,則會(huì)輸出類似如下所示的信息
# FIRMWARE_NAME:Marlin 1.1.0 ....
if not self._firmware_name_requested:
self._firmware_name_requested = True
self.sendCommand("M115")
# 獲取FIRMWARE_NAME并保存起來
if b"FIRMWARE_NAME:" in line:
self._setFirmwareName(line)
# time()是獲取時(shí)間戳,以秒作為時(shí)間間隔,這里的timeout是3,也就意味著,Cura發(fā)送獲取溫度的條件是:
# 1、當(dāng)前的打印機(jī)不處于忙狀態(tài)
# 2、超時(shí),這里設(shè)置的時(shí)間是大于3s
# 以上兩個(gè)條件需要同時(shí)滿足
if self._last_temperature_request is None or time() > self._last_temperature_request + self._timeout:
self.sendCommand("M105")
self._last_temperature_request = time()
# 使用正則表達(dá)式獲取由打印機(jī)端上報(bào)的溫度事件,其中T:開頭的數(shù)據(jù)代表噴頭溫度,B:開頭的數(shù)據(jù)代表熱床溫度
if re.search(b"[B|Td*]: ?d+.?d*", line): # Temperature message. 'T:' for extruder and 'B:' for bed
extruder_temperature_matches = re.findall(b"T(d*): ?(d+.?d*)s*/?(d+.?d*)?", line)
# Update all temperature values
# 獲取噴頭當(dāng)前/目標(biāo)溫度值并更新到前端顯示
matched_extruder_nrs = []
for match in extruder_temperature_matches:
extruder_nr = 0
if match[0] != b"":
extruder_nr = int(match[0])
if extruder_nr in matched_extruder_nrs:
continue
matched_extruder_nrs.append(extruder_nr)
if extruder_nr >= len(self._printers[0].extruders):
Logger.log("w", "Printer reports more temperatures than the number of configured extruders")
continue
extruder = self._printers[0].extruders[extruder_nr]
if match[1]:
extruder.updateHotendTemperature(float(match[1]))
if match[2]:
extruder.updateTargetHotendTemperature(float(match[2]))
# 獲取熱床當(dāng)前/目標(biāo)溫度值并更新到前端顯示
bed_temperature_matches = re.findall(b"B: ?(d+.?d*)s*/?(d+.?d*)?", line)
if bed_temperature_matches:
match = bed_temperature_matches[0]
if match[0]:
self._printers[0].updateBedTemperature(float(match[0]))
if match[1]:
self._printers[0].updateTargetBedTemperature(float(match[1]))
# 空行表示固件空閑
# 多個(gè)空行可能意味著固件和 Cura 正在等待
# 因?yàn)殄e(cuò)過了“ok”,所以我們跟蹤空行
# 因?yàn)閛k可能丟掉了,所以我們需要將空行記錄下來
if line == b"":
# An empty line means that the firmware is idle
# Multiple empty lines probably means that the firmware and Cura are waiting
# for each other due to a missed "ok", so we keep track of empty lines
self._firmware_idle_count += 1
else:
self._firmware_idle_count = 0
# 檢查到ok字串或者_(dá)firmware_idle_count > 1
if line.startswith(b"ok") or self._firmware_idle_count > 1:
# 此時(shí)打印機(jī)忙狀態(tài)解除
self._printer_busy = False
# 設(shè)置接收事件為True
self._command_received.set()
# 如果當(dāng)前命令隊(duì)列不為空,則從隊(duì)列取出一條命令往打印機(jī)串口繼續(xù)發(fā)送
if not self._command_queue.empty():
self._sendCommand(self._command_queue.get())
# 如果處于正在打印中,則繼續(xù)發(fā)送下一條Gcode命令
# 如果此時(shí)暫停標(biāo)志生效,則什么事情都不干
elif self._is_printing:
if self._paused:
pass # Nothing to do!
else:
self._sendNextGcodeLine()
# 如果匹配到Marlin回復(fù)了"echo:busy"子串時(shí),則設(shè)置打印機(jī)為忙狀態(tài)
if line.startswith(b"echo:busy:"):
self._printer_busy = True
# 如果在打印中接收到'!!',則表示打印機(jī)發(fā)出致命錯(cuò)誤,這個(gè)時(shí)候需要直接取消打印
if self._is_printing:
if line.startswith(b'!!'):
Logger.log('e', "Printer signals fatal error. Cancelling print. {}".format(line))
self.cancelPrint()
# 如果在打印中接收到"resend"或者"rs"這樣的字符串,則可以通過 Resend、resend 或 rs 請(qǐng)求重新發(fā)送。
elif line.lower().startswith(b"resend") or line.startswith(b"rs"):
# A resend can be requested either by Resend, resend or rs.
try:
self._gcode_position = int(line.replace(b"N:", b" ").replace(b"N", b" ").replace(b":", b" ").split()[-1])
except:
if line.startswith(b"rs"):
# In some cases of the RS command it needs to be handled differently.
self._gcode_position = int(line.split()[1])
在USB轉(zhuǎn)串口聯(lián)機(jī)打印中,也實(shí)現(xiàn)了一些打印的基本業(yè)務(wù),待后續(xù)分析和開源作品分享。