home
erste Version am 14.03.2019
letzte Änderung am 20.03.2019

GCode-View


Kürzlich ist es mir beim ersten Druck eines 3D-Modells mal wieder passiert, dass der generierte GCode Bewegungsmuster enthielt, die das Aussehen des Druckobjekts verdorben haben. Besonders ärgerlich ist sowas, wenn es erst zum Ende des Drucks passiert - also nach zwei/drei Stunden Druckdauer.
Natürlich hätte ich das Problem im Voraus erkennen können, wenn ich vor dem Start des Drucks im Repetier-Server unter Print/2D-Preview nachgesehen hätte. Jedoch läuft der Repetier-Server in meinem Fall auf einem RasPi3 und ist damit nicht gerade sonderlich performant. Auch kann man in dieser Ansicht nicht wirklich gut zwischen normalen "Travel-Moves" und "Travel-Moves nach Retraktion" unterscheiden.
Kurzum: ich brauche ein Programm, dass mir eine komfortable GCode-Vorschau bietet.


Inhaltsverzeichnis

erste Version
Version 1.1
Version 1.2


erste Version

Die erste Version stand nach knapp acht Stunden. Sie berücksichtigt die Kommandos G0, G1 und G28. Hinter den Kommandos werden der X-, Y-, Z- und E-Parameter verarbeitet.
Auch wird davon ausgegangen, dass der Wert des Z-Parameters konstant steigt. Somit würde solcher GCode nicht verarbeitet werden können, bei dem vor Travel-Moves jeweils ein Z-Lift erfolgt. In Cura gibt diese Option...um die einzuschalten, braucht es aber wohl sehr großes Vertrauen in die Präzision der Z-Spindeln.
Apropos Cura: ich nutze Cura v3.6.0 (bzw. neuerdings v4.0.0) und meine beiden Drucker laufen mit "Marlin" (unter "Machine Settings/G-code flavor"). Ich habe keine Ahnung, ob mein Programm auch mit anderen GCode-flavors klarkommt.

Dass auch der Wert hinter dem E-Parameter innerhalb eines Druckjobs konstant steigt, ist mir erst bei der GCode-Analyse zu diesem Programm aufgefallen. Vorher war ich einfach davon ausgegangen, dass die Menge des extrudierten Filaments als absoluter Wert pro Move-Kommando angegeben wird. Das hätte die Erkennung von "Retraktion vor Travel-Move" vereinfacht. So muss nun eine Variable geführt werden, die immer den vorigen E-Wert enthält, um bei kleinerem aktuellen E-Wert eine Retraktion detektieren zu können.

Die Funktion zum Laden und Vorverarbeiten einer GCode-Datei ist noch nicht auf Geschwindigkeit optimiert. Für eine GCode-Datei mit 21MB bzw. 727.713 Zeilen werden 0.13 Sekunden benötigt, um sie in eine Python-Liste zu laden. Die anschließende Konvertierung dieser Daten benötigt jedoch derzeit noch 2.95 Sekunden und ist damit deutlich störend. Final sollte das Laden der Datei vielleicht als Thread laufen, der sein erstes Ergebnis bereits nach Abarbeitung des ersten Layers liefert.
Bei der Vorverarbeitung wird eine Liste mit einem Element pro Layer erzeugt. Jedes dieser Elemente ist wiederum eine Liste aus Tupeln mit den Werten für (X, Y, Z, E).

Folgendes Bild entsteht, wenn ich den GCode für dieses Thing in die Anzeige lade (Bild anklicken für volle Größe):
Screenshot gcv
Schwarze Linien kennzeichnen Extraktions-Moves, gelbe Linien Travel-Moves und blaue Linien kennzeichnen Travel-Moves mit vorheriger Retraktion.
Mit der Y-Scrollbar werden die Layer gewechselt, die X-Scrollbar zeigt die Wege der Nozzle pro Layer.

Und hier mal ein Durchmarsch durch die 515 Layer eines weinenden Engels, wenn man ihn denn mit 25% Infill drucken wollen würde:


Der Programm-Kode sieht folgendermaßen aus:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# ###########################################################
# gcv.py
#
# Ein Script zum visualisieren einer GCode-Datei.
#
#
# Detlev Ahlgrimm, 2019
#
# 12/13.03.2019 erste Version

import wx
import time
#import numpy as np

t_g=0

VERSION="1.0"
CFG_FILE=".gcv.rc"

inputfile="/home/dede/CFFFP_WBGHfdGSM_v1.01.gcode"
#inputfile="/home/dede/CFFFP_Badezimmer-Sperre.gcode"
#inputfile="/home/dede/CFFFP_weeping_angel_attacking.gcode"

# ###########################################################
#
def parseLine(ln):
X=Y=Z=E=None
for e in ln:
if e.startswith(";"):
break
if e.startswith("X"):
X=float(e[1:])
if e.startswith("Y"):
Y=float(e[1:])
if e.startswith("Z"):
Z=float(e[1:])
if e.startswith("E"):
E=float(e[1:])
return X, Y, Z, E


# ###########################################################
#
def getFile():
global t_g
with open(inputfile, "r") as fh:
filedata=fh.readlines()
t=time.time()
#print("load done", t-t_g)
t_g=t

lastE=0
retract=False
layers=[]
cur_layer=[]
for ln in filedata:
lns=ln.split()
if len(lns)>0 and lns[0]=="G28": # home all axis
if len(cur_layer)>1: layers.append(cur_layer)
cur_layer=[]

if len(lns)>0 and (lns[0]=="G0" or lns[0]=="G1"):
X, Y, Z, E=parseLine(lns)
if E is not None:
if E<lastE:
retract=True
lastE=E

if Z is not None: # Z-Move
if len(cur_layer)>1: layers.append(cur_layer)
if retract:
if E is not None:
retract=False
cur_layer=[(X, Y, Z, E)]
else:
cur_layer=[(X, Y, Z, -1)]
else:
cur_layer=[(X, Y, Z, E)]
elif X is not None and Y is not None: # X/Y-Move
if retract:
if E is not None:
retract=False
cur_layer.append((X, Y, Z, E))
else:
cur_layer.append((X, Y, Z, -1))
else:
cur_layer.append((X, Y, Z, E))
if len(cur_layer)>1: layers.append(cur_layer)


t=time.time()
#print("processing done", t-t_g)
t_g=t

# for l in layers:
# print l[:min(4, len(l))]

return(layers)




# ###########################################################
# Das Fenster.
class GCVPanel(wx.Panel):
def __init__(self, parent):
super(GCVPanel, self).__init__(parent)

self.parent=parent
self.hsizer=wx.BoxSizer(wx.HORIZONTAL)
self.vsizer=wx.BoxSizer(wx.VERTICAL)

self.Bind(wx.EVT_PAINT, self.on_paint)
self.Bind(wx.EVT_SIZE, self.on_size)
self.Bind(wx.EVT_TIMER, self.on_timer)
self.timer=wx.Timer(self)

self.scrollbarV=wx.ScrollBar(self, size=(-1, -1), style=wx.SB_VERTICAL)
self.scrollbarV.Bind(wx.EVT_SCROLL, self.on_vscroll)

self.scrollbarH=wx.ScrollBar(self, size=(-1, -1), style=wx.SB_HORIZONTAL)
self.scrollbarH.Bind(wx.EVT_SCROLL, self.on_hscroll)

self.hsizer.AddStretchSpacer()
self.hsizer.Add(self.scrollbarV, 0, flag=wx.EXPAND|wx.ALL)
self.vsizer.Add(self.hsizer, 1, flag=wx.EXPAND| wx.ALL)
self.vsizer.Add(self.scrollbarH, 0, flag=wx.EXPAND| wx.ALL)
self.SetSizer(self.vsizer)

self.dc=None
self.layerData=None
wx.CallLater(50, self.WarteAufDC) # "on_paint" muss gelaufen sein

self.cnt=0
self.m=[]
self.p=[]
self.base=(10, 10)

self.pen_extr=wx.Pen("#000000")
self.pen_trav=wx.Pen("#A3F807")
self.pen_retr=wx.Pen("#0055F9")


# ###########################################################
# Ruft sich so lange selbst wieder auf, bis "self.dc" in
# "on_paint" gesetzt wurde, um dann die Datei zu laden.
def WarteAufDC(self):
if self.dc==None:
wx.CallLater(50, self.WarteAufDC)
else:
self.layerData=getFile()

self.m, self.p=self.getLayerAsLineListAndPenList(self.layerData, 0, 1050)
#self.timer.Start(10)
self.cnt=len(self.m)
self.scrollbarH.SetScrollbar(len(self.m), 1, len(self.m), 1)
self.scrollbarV.SetScrollbar(len(self.layerData), 1, len(self.layerData), 1)
self.Refresh(False)

t=time.time()
#print("screen ready", t-t_g)


# ###########################################################
# Ruft einmal pro Update-Intervall die Anzeige-Update-Funktion
# auf.
def on_timer(self, event):
self.cnt+=1
self.Refresh(False)

# ###########################################################
# Fenster-Grössen-Änderung verarbeiten.
def on_size(self, event):
self.Refresh(False)
event.Skip()

# ###########################################################
#
def on_hscroll(self, event):
tp=self.scrollbarH.GetThumbPosition()
self.cnt=tp
self.Refresh(False)

# ###########################################################
#
def on_vscroll(self, event):
tp=len(self.layerData)-1-self.scrollbarV.GetThumbPosition()
self.m, self.p=self.getLayerAsLineListAndPenList(self.layerData, tp, 1050)
self.cnt=len(self.m)
self.scrollbarH.SetScrollbar(len(self.m), 1, len(self.m), 15)
self.Refresh(False)

# ###########################################################
# Aktualisiert das Fenster.
def on_paint(self, event):
self.dc=wx.AutoBufferedPaintDC(self)
self.dc.SetBackground(wx.Brush("#CCCCCC"))
self.dc.Clear()
self.dc.SetPen(wx.Pen("#FFFFFF"))
for x in range(0, 211, 10):
self.dc.DrawLine(self.base[0]+x*5, self.base[1], self.base[0]+x*5, self.base[1]+210*5)
self.dc.DrawLine(self.base[0], self.base[1]+x*5, self.base[0]+210*5, self.base[1]+x*5)

if self.cnt<len(self.m):
self.dc.DrawLineList(self.m[:self.cnt], self.p[:self.cnt])
self.dc.DrawCircle(self.m[self.cnt][2], self.m[self.cnt][3], 4)
else:
self.timer.Stop()
self.dc.DrawLineList(self.m, self.p)

# ###########################################################
#
def getLayerAsLineListAndPenList(self, layers, layer_number, yh):
layer=layers[layer_number]
move=[]
pen=[]
if layer_number==0:
lastX, lastY=self.base[0], self.base[1]+yh
else:
lastX, lastY, _, _=layers[layer_number-1][-1]
lastX=self.base[0]+int(lastX*5)
lastY=self.base[1]+yh-int(lastY*5)

for p in layer:
X, Y, Z, E=p
X=self.base[0]+int(X*5)
Y=self.base[1]+yh-int(Y*5)

move.append((lastX, lastY, X, Y))
if E is None or E<0: # Travel-Move
if E is None:
pen.append(self.pen_trav)
else:
pen.append(self.pen_retr)
else: # Extrusion-Move
pen.append(self.pen_extr)
lastX, lastY=X, Y
return move, pen



# ###########################################################
# Der Fenster-Rahmen fuer das Hauptfenster.
class GCVFrame(wx.Frame):
def __init__(self, parent, pos=wx.DefaultPosition, size=wx.DefaultSize):
style=wx.DEFAULT_FRAME_STYLE
#if FRAME_NO_TASKBAR==True:
# style|=wx.FRAME_NO_TASKBAR
wx.Frame.__init__(self, None, wx.ID_ANY, "GCode-View v"+VERSION, pos=pos, size=size, style=style)
self.panel=GCVPanel(self)


# ###########################################################
# Der Starter
if __name__=='__main__':
t_g=time.time()
#print("start", t_g)
fc=wx.FileConfig(localFilename=CFG_FILE)
spx=fc.ReadInt("pos_x", -1)
spy=fc.ReadInt("pos_y", -1)
ssx=fc.ReadInt("size_x", 1100)
ssy=fc.ReadInt("size_y", 1100)

sp=(spx, spy) # (-1, -1) entspricht wx.DefaultPosition
ss=(ssx, ssy) # (-1, -1) entspricht wx.DefaultSize

app=wx.App(False)
frame=GCVFrame(None, pos=sp, size=ss)
frame.Show(True)
app.MainLoop()

Für die V1.1 steht dann an, dass GCode-Dateien per Drag&Drop geladen werden können. Vor allen Dingen muss aber hinzukommen, dass Größenänderungen des Fensters zu einer Skalierung des dargestellten Modells führen.

Das Hinein-Zoomen in das Modell (vorzugsweise mit dem Mausrad) spare ich mir dann für die V1.2 auf.
Und irgendwann kommt dann vielleicht noch etwas Kosmetik hinzu. Also etwa per Kontextmenü die Farben anpassen oder die Größe des Heizbetts ändern. Oder auch die Geschwindigkeits-Optimierung der Daten-Vorverarbeitung.


Version 1.1

Die angedachten Erweiterungen für die V1.1 waren nach einer Stunde eingebaut. Das waren mir entschieden zu wenig Änderungen für eine eigene, neue Version.
Deshalb enthält die jetzige V1.1 auch bereits die für die V1.2 angedachten Erweiterungen - außer der Geschwindigkeits-Optimierung. Als zusätzliches Goodie kann man das Modell bzw. das Heizbett jetzt auch im Fenster verschieben. Die Bedienung funktioniert quasi genau so, wie bei OpenStreetMap.

Mittlerweile hat die Datei 595 Zeilen und ist damit deutlich zu lang für so einen scrollbaren "Code-Holder" wie bei der V1.0.
Daher hier jetzt ein ZIP-Archiv mit der V1.1.


Version 1.2

Nun ist auch noch die Vorverarbeitung der GCode-Datei per Thread implementiert.
Damit ist das Programm jetzt sofort bedienbar - auch wenn noch nicht alle Layer vorverarbeitet sind.
Und hier noch ein Video, das die Bedienung des Programms anhand des selben Things veranschaulicht, welches schon in obigem Screenshot genutzt wurde:
Anbei noch das Archiv für die V1.2.

Eigentlich ist dieses Projekt damit jetzt abgeschlossen.
Aber vielleicht werden mir bei der Nutzung des Programms ja noch ein paar Dinge auffallen, die ich auch drin haben möchte....