加入星計(jì)劃,您可以享受以下權(quán)益:

  • 創(chuàng)作內(nèi)容快速變現(xiàn)
  • 行業(yè)影響力擴(kuò)散
  • 作品版權(quán)保護(hù)
  • 300W+ 專業(yè)用戶
  • 1.5W+ 優(yōu)質(zhì)創(chuàng)作者
  • 5000+ 長(zhǎng)期合作伙伴
立即加入
  • 正文
  • 相關(guān)推薦
  • 電子產(chǎn)業(yè)圖譜
申請(qǐng)入駐 產(chǎn)業(yè)圖譜

3D打印機(jī)USB聯(lián)機(jī)打印是如何實(shí)現(xiàn)的?(以Cura插件USBPrinting為例)

2022/09/27
2210
閱讀需 22 分鐘
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點(diǎn)資訊討論
來(lái)源 | 嵌入式應(yīng)用研究院

整理&排版 | 嵌入式應(yīng)用研究院

眾所周知,對(duì)3D打印機(jī)感興趣的小伙伴來(lái)說(shuō),都清楚Cura是3D打印機(jī)的切片軟件,它的UI部分是基于QT來(lái)開發(fā)的。而Cura中很多功能其實(shí)是基于插件的形式來(lái)開發(fā),其中,用于實(shí)現(xiàn)Cura的USB轉(zhuǎn)串口聯(lián)機(jī)打印的邏輯就是一個(gè)插件,它是使用Python語(yǔ)言來(lái)實(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)方案

說(shuō)到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)更新線程來(lái)處理串口數(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ā)為止。

處理打印過(guò)程中發(fā)生的異常事件

發(fā)送M105獲取溫度命令,這里Cura是做了一些處理的,發(fā)送該條命令的前提是打印機(jī)不處于忙狀態(tài)并且溫度到了設(shè)定的固件超時(shí)時(shí)間才會(huì)進(jìn)行發(fā)送。Cura的超時(shí)設(shè)置為3s。

Gcode重發(fā)機(jī)制的實(shí)現(xiàn)

具體的代碼實(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并保存起來(lái)
    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ò)過(guò)了“ok”,所以我們跟蹤空行
            # 因?yàn)閛k可能丟掉了,所以我們需要將空行記錄下來(lái)
            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"這樣的字符串,則可以通過(guò) 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ù)分析和開源作品分享。

相關(guān)推薦

電子產(chǎn)業(yè)圖譜

本科畢業(yè)于華南理工大學(xué),現(xiàn)美國(guó)卡羅爾工商管理碩士研究生在讀,曾就職于世界名企偉易達(dá)、聯(lián)發(fā)科技等,多年嵌入式產(chǎn)品開發(fā)經(jīng)驗(yàn),在智能玩具、安防產(chǎn)品、平板電腦、手機(jī)開發(fā)有豐富的實(shí)戰(zhàn)開發(fā)經(jīng)驗(yàn),現(xiàn)任深圳市云之手科技有限公司副總經(jīng)理、研發(fā)總工程師。