<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Testing on Schallberts Blog</title><link>https://blog.schallbert.de/tags/testing/</link><description>Recent content in Testing on Schallberts Blog</description><generator>Hugo</generator><language>de</language><lastBuildDate>Sat, 04 May 2024 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.schallbert.de/tags/testing/index.xml" rel="self" type="application/rss+xml"/><item><title>CNC Tuning #3</title><link>https://blog.schallbert.de/cnc-tuning-pathinterpreter/</link><pubDate>Sat, 04 May 2024 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/cnc-tuning-pathinterpreter/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-20-G64RD-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Thumbnail of endmill cutting forces"&#10; title="CNC Tuning #3" /&gt;&#10;&lt;p&gt;Dies ist der letzte Teil meiner Reihe über die Optimierung der Fertigungszeit auf CNC-Fräsmaschinen. Er konzentriert sich auf den Pfadinterpreter der CNC-Steuersoftware.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/cnc-tuning-gcode/"&gt;&lt;em&gt;Teil 1&lt;/em&gt;: Besserer G-Code&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/cnc-tuning-kinematics/"&gt;&lt;em&gt;Teil 2&lt;/em&gt;: Kinematische Parameter hochdrehen&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Der Pfadinterpreter der Steuersoftware ist dafür zuständig, die Anweisungen aus dem &lt;code&gt;G-Code&lt;/code&gt; zu lesen und in Maschinenbewegungen zu übersetzen. Soll beispielsweise &lt;code&gt;G01 X100 Y200 Z-2.0 F2000 S18000&lt;/code&gt; ausgeführt werden, so muss der Pfadinterpreter die akuelle Position der Maschine kennen und einen Fräsvektor erzeugen, der zur gewünschten Position mit den vorgegebenen Parametern zeigt.&lt;/p&gt;&#10;&lt;p&gt;Hierbei muss der Interpreter die kinematischen Parameter der Maschine berücksichtigen: Mit welcher Rampe auf Zielgeschwindigkeit &lt;code&gt;F1000&lt;/code&gt; beschleunigt wird ob der gewünschte Vorschub überhaupt zulässig ist, und ab welchem Punkt wieder gebremst werden muss um den Zielpunkt exakt zu erreichen. Die so gewonnenen Daten werden schließlich in eine Schrittfolge für die drei Schrittmotoren übersetzt, in einen Pufferspeicher geladen und schließlich in Echtzeit an die Motorendstufen ausgegeben.&lt;/p&gt;&#10;&lt;h2 id="schritt-3-optimierung-des-pfadinterpreters"&gt;Schritt 3: Optimierung des Pfadinterpreters&lt;/h2&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-05-04-laf-settings.jpg" alt="Image: Look Ahead Feed in the CNC control software" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Viele Steuersoftware-Anbieter haben integrierte Fräspfadfilter programmiert, welche mittels Kommando &lt;code&gt;G64&lt;/code&gt; oder per Menü zu konfigurieren sind. Ich verlinke hier mal ein paar Beispiele verschiedener Hersteller, wo für spezifische Details nachgelesen werden kann:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://www.datron.com/resources/blog/toolpath-filter-achieves-a-perfect-surface-finish/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&amp;ldquo;Toolpath filter&amp;rdquo; von Datron&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://docs.edingcnc.com/settings/trajectory" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&amp;ldquo;Look Ahead Feed&amp;rdquo; von EdingCNC&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="http://wiki.linuxcnc.org/cgi-bin/wiki.pl?TrajectoryControl" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&amp;ldquo;Path Blending&amp;rdquo; von LinuxCNC&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Die Kunst ist nun, eine Konfiguration passend zum Fräsjob und der Maschinenkinematik zu finden. Es geht hier immer um einen Kompromiss zwischen Fertigungsgeschwindigkeit und Bahntoleranz. Soll also mit hohem Vorschub gefahren werden, werden eigentlich spitze Kanten verrundet, verbundene Liniensegmente in einem Toleranzband angefahren oder Pfade so vereinfacht, dass außerhalb der von der Maschine erreichbaren Beschleunigung befindliche Pfadpunkte nicht angefahren werden.&lt;/p&gt;&#10;&lt;h3 id="grundlagen-was-ist-look-ahead-feed"&gt;Grundlagen: Was ist &amp;ldquo;Look Ahead Feed&amp;rdquo;?&lt;/h3&gt;&#10;&lt;p&gt;Hiermit ist gemeint, dass der Pfadinterpreter der Steuersoftware sich während des Ausführens einer Fräsoperation - z.B. &lt;code&gt;G01 X* Y* (lineare Bewegung Punkt zu Punkt)&lt;/code&gt; - bereits die nächste(n) Operation(en) anschaut und herausfindet, ob und wie stark zwischen den Operationen abgebremst und wieder beschleunigt werden muss bzw. direkt durchgefahren werden kann. Die Physik dahinter erkläre ich &lt;a href="https://blog.schallbert.de/portal-milling-setup/#acceleration"&gt;im Artikel zum CNC-Setup&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Hierdurch kann teils massiv Zeit gespart werden, der Fräsvorgang wird flüssiger und die Maschine erfährt geringere Belastungen. Zusätzlich zur Vermeidung von Beschleunigungs- und Bremsvorgängen kann mittels Parameter &lt;code&gt;G64&lt;/code&gt; eingestellt werden, ob und wie Fräspfadfilter die geplante Trajektorie verändern dürfen, um weiter auf höhere Bahngeschwindigkeiten zu optimieren.&lt;/p&gt;&#10;&lt;h3 id="ideal-im-gravurbetrieb"&gt;Ideal im Gravurbetrieb&lt;/h3&gt;&#10;&lt;p&gt;Im Gravurbetrieb kann man sich solche Optionen hervorragend zu Nutze machen: Höhere Toleranzen sind hier meist OK, und leicht verrundete Kanten oft akzeptabel. Bei 3D-Gravuren dauert das Fräsen durch die vielen Pfade lang, und hier ist besonders viel Zeit herauszuholen.&lt;/p&gt;&#10;&lt;p&gt;Da ich Eding CNC benutze, kann ich nur für Parameter dieser Steuerung Tipps geben. Nun aber ab ins Detail zur Erklärung der Einstellungen!&lt;/p&gt;&#10;&lt;h3 id="fräspfadtoleranz-g64-p"&gt;Fräspfadtoleranz &lt;code&gt;G64 P*&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-05-04-lafblending.jpg" alt="Image: Look Ahead Feed G64 CNC corner blending" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Dieser Parameter gibt der Steuersoftware die allgemeine Toleranz der Pfadgenauigkeit vor.&lt;/p&gt;&#10;&lt;p&gt;Ich habe meine Maschine so eingestellt, dass sie im Gravurbetrieb Abweichungen vom vorgegebenen Fräspfad von bis zu &lt;code&gt;LAF blending tolerance = 0.3mm&lt;/code&gt; zulässt. Alternativ kann die Konfiguration per Kommando &lt;code&gt;G64 P0.3&lt;/code&gt; erfolgen. Im Bild habe ich skizziert, wie sich eine solche Einstellung in der Praxis auswirken kann: Der Fräspfad (weiß) besteht aus &lt;code&gt;G01&lt;/code&gt;-Liniensegmenten. Die Toleranz ist mit Hilfe von Kreisen eingezeichnet. Der Pfadinterpreter der Steuersoftware kann nun eigenständig Kreissegmente so definieren, dass innerhalb der Toleranz eine möglichst hohe Bahngeschwindigkeit erreicht wird.&lt;/p&gt;&#10;&lt;p&gt;Die so ermittelte Bahn (türkis) ähnelt einer Spline-Kurve. Zu beachten ist hier, dass sämtliche Linien von dieser Einstellung betroffen sein können - auch solche, die strenge Toleranzanforderungen haben. Ist dies nicht gewünscht, so sollte hier ein kleinerer Wert verwendet werden.&lt;/p&gt;&#10;&lt;h3 id="pfadverrundung-g64-r"&gt;Pfadverrundung &lt;code&gt;G64 R*&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;Treffen zwei Liniensegmente im Winkel von bis zu &lt;code&gt;LAF full speed blending angle threshold = 12°&lt;/code&gt; aufeinander, wird die Maschine ohne abzubremsen versuchen, dem Winkel genau zu folgen. Bei größeren Winkeln wird mit reduzierter Geschwindigkeit mit per &lt;code&gt;G64 P*&lt;/code&gt; definierter Toleranz gefahren. Diese Konfiguration wird mit &lt;code&gt;G64 R12&lt;/code&gt; programmiert.&lt;/p&gt;&#10;&lt;p&gt;Die Werte hier sind mit großer Vorsicht einzustellen, weil sie zu Beschleunigungsspitzen auf der Maschine führen. Große Werte können Schrittverluste und höheren Verschleiß der CNC-Fräse verursachen. Dafür werden Bahngeschwindigkeit und Genauigkeit optimal gehalten.&lt;/p&gt;&#10;&lt;h3 id="pfadverrundung-bei-reduziertem-vorschub-g64-s-d"&gt;Pfadverrundung bei reduziertem Vorschub &lt;code&gt;G64 S* D*&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-05-04-G64RD.jpg" alt="Image: Look Ahead Feed G64 CNC corner radius" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Diese Konfiguration gibt der Steuersoftware die Möglichkeit, Kanten bis zum eingestellten Toleranzwert zu verrunden: &lt;code&gt;LAF reduced speed path smoothing delta = 0.001mm&lt;/code&gt;. In Kombination mit der Verrundung muss der Winkel eingestellt werden, bis zu dem diese Verrundung durchgeführt werden soll: &lt;code&gt;LAF reduced speed path smoothing angle threshold = 0°&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Programmiert wird diese Funktion am Besten in Kombination mit der Pfadverrundung bei vollem Vorschub als &lt;code&gt;G64 R* S* D*&lt;/code&gt;. Der Vorteil hierbei ist, dass Liniensegmenten wie im Bild gezeigt sehr genau gefolgt wird, und die Kanten gleichzeitig aber fein justierbar abgerundet werden. Bis zum über &lt;code&gt;R&lt;/code&gt; definierten Winkel wird sogar mit vollem Vorschub dem Pfad exakt gefolgt.&lt;/p&gt;&#10;&lt;p&gt;Werden bei &lt;code&gt;S&lt;/code&gt; sehr große Winkel eingestellt, so findet entsprechend &lt;code&gt;D&lt;/code&gt; auch eine stärkere Verrundung statt. Überschreitet der im Fräsprogramm vorgegebene Winkel jedoch den konfigurierten Wert, so fällt der Algorithmus auf die für &lt;code&gt;P&lt;/code&gt; hinterlegten Werte bzw. in den exakten Pfadfolgemodus zurück.&lt;/p&gt;&#10;&lt;h3 id="pfadvereinfachung-g64-q"&gt;Pfadvereinfachung &lt;code&gt;G64 Q*&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-05-04-lafcombine.jpg" alt="Image: Look Ahead Feed G64, path combine setting" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Enthält die G-Code Datei zahlreiche, sehr kurze Liniensegmente (wie sie oft bei der Erstellung von Pfaden aus Bilddateien entstehen), kann diese Funktion durch Vereinfachen der Fräspfade Abhilfe schaffen. Dabei gibt die Zahl hinter dem Parameter die Toleranz an, bis zu der Liniensegmente zu einer Linie verschmolzen werden.&lt;/p&gt;&#10;&lt;p&gt;Diese Einstellung entlastet den Pufferspeicher der Maschine und erhöht gleichzeitig die mittlere Bahngeschwindigkeit. Für den Gravurbetrieb halte ich diese Einstellung insbesondere für interessant, wenn mit &amp;ldquo;verrauschten&amp;rdquo; Daten gearbeitet wird und im CAD nicht die Möglichkeit bestand, die Anzahl der Pfade zu reduzieren.&lt;/p&gt;&#10;&lt;h3 id="beschleunigungsfilter-g64-f"&gt;Beschleunigungsfilter &lt;code&gt;G64 F*&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;Mittels diesen Parameters können die durch Look Ahead Feed möglicherweise erzeugten Beschleunigungsspitzen abgemildert werden. Der minimal einzustellende Wert ergibt sich durch den Quotienten aus gewähltem Vorschub für die Operation und maximaler Geschwindigkeit der Maschine.&lt;/p&gt;&#10;&lt;p&gt;Beispiel: &lt;code&gt;Fmin = Foperate / Fg00&lt;/code&gt;, bei &lt;code&gt;Fg00 = 12000mm/min&lt;/code&gt; und &lt;code&gt;Foperate = 5000mmm/min&lt;/code&gt; ist &lt;code&gt;Fmin = 0.42&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;Also wird bei Vorschub von &lt;code&gt;5000mm/min&lt;/code&gt; die maximal mögliche Beschleunigung der Maschine abgerufen und Kurvenradien so eingestellt, dass sie bei konstanter Bahngeschwindigkeit gefahren werden. Alle Werte größer gleich &lt;code&gt;Fmin = 1&lt;/code&gt; sind inherent sicher, unabhängig vom gewählten Vorschub.&lt;/p&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-05-04-accelfilter.jpg" alt="Image: acceleration filter for G64 operation" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Dieser Parameter wurde entwickelt für die Bearbeitung von elastischen Materialien wie Gummi oder Kork, wo eine konstante Vorschubgeschwindigkeit extrem wichtig und die Bahntreue eher zweitrangig ist. Der Parameter wird in Kombination mit &lt;code&gt;G64 R*&lt;/code&gt; verwendet. Die folgende Programmierung für das obige Beispiel würde die volle Kapazität der Maschine abrufen: &lt;code&gt;G64 R120 F0.42&lt;/code&gt;, wobei mit &lt;code&gt;F5000&lt;/code&gt; gefahren wird, &lt;code&gt;F12000&lt;/code&gt; der Maximalvorschub der Maschine ist und spitze Winkel bis &lt;code&gt;120°&lt;/code&gt; ohne abzubremsen gefahren werden sollen.&lt;/p&gt;&#10;&lt;p&gt;Wende ich diese Werte auf meine Maschine an, entsteht das nebenstehende Bild. Hier soll ein Rechteck mit &lt;code&gt;25 x 100mm&lt;/code&gt; Kantenlänge gefertigt werden. Weiß ist wieder die Zieltrajektorie dargestellt. Die türkise Kurve zeigt die durch die Programmierung des Beschleunigungsfilters gefahrene Kurve mit konstanter Bahngeschwindigkeit. Je geringer der Vorschub im Verhältnis zum Maximalvorschub und je höher die Beschleunigung der Maschine eingestellt ist, desto kleiner wird der notwendige Radius.&lt;/p&gt;&#10;&lt;p&gt;Für den Gravurbetrieb und das Fräsen fester Materialien wird dieser Parameter meiner Erfahrung nach jedoch nicht verwendet.&lt;/p&gt;&#10;&lt;h2 id="meine-konfiguration-für-die-hobbyline-4530"&gt;Meine Konfiguration für die Hobbyline 4530&lt;/h2&gt;&#10;&lt;p&gt;Ich habe im Gravurbetrieb viel mit den verschiedenen LAF-Parametern herumgespielt und eine Menge Rohlinge als Lehrgeld &amp;ldquo;verbraucht&amp;rdquo;. Die kürzesten Fertigunszeiten erhielt ich bei vorheriger Optimierung der kinematischen Parameter mit folgender Konfiguration:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&lt;code&gt;G64 Q0.3 R12 D0.3 S120&lt;/code&gt;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Damit sage ich der Maschine folgendes:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Verwende den Pfadvorschau-Algorithmus und versuche, kurze Liniensegmente unter &lt;code&gt;0.3mm&lt;/code&gt; Länge zu verschmelzen. Versuche, den Vorschub bei einem Winkel zweier Streckenabschnitte von bis zu &lt;code&gt;12°&lt;/code&gt; nicht herunterzuregeln. Bei größeren Winkeln bis &lt;code&gt;120°&lt;/code&gt; kann statt einem exakten Nachfahren mit bis zu &lt;code&gt;r=0.3mm&lt;/code&gt; verrundet werden. Bei noch größeren Winkeln ist dem vorgegebenen Pfad zu folgen und, falls erforderlich, bis zum Stillstand abzubremsen.&amp;rdquo;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h2 id="fazit"&gt;Fazit&lt;/h2&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-05-04-duplicated-software.jpg" alt="Image: dual install of CNC control software for different purposes" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Da mich die Parameteroptimierung für den Gravurbetrieb ziemlich weit vom Parametersatz für den normalen Fräsbetrieb weggebracht haben, habe ich mein Steuerprogramm einfach ein zweites Mal installiert.&lt;/p&gt;&#10;&lt;p&gt;Dort konnte ich dann Geschwindigkeit, Achsbeschleunigung und Pfadparametrierung getrennt festlegen. Nun habe ich das Beste beider Welten auf einer Maschine zur Verfügung:&lt;/p&gt;&#10;&lt;p&gt;Einen sehr genauen, weich operierenden Fräsbetrieb mit folgenden Parametern:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;Fmax XY 12000mm/min&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Fmax Z 4800mm/min&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Amax X 1000mm/s²&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Amax Y 1000mm/s²&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Amax Z 1000mm/s²&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;G64 P0.1 R6&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Und über die andere Verknüpfung einen überaus flinken Gravurbetrieb:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;Fmax XY 12000mm/min&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Fmax Z 4800mm/min&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Amax X 4000mm/s²&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Amax Y 3200mm/s²&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Amax Z 1600mm/s²&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;G64 Q0.3 R12 D0.3 S120&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="und-wo-ist-der-beweis"&gt;Und wo ist der Beweis?&lt;/h2&gt;&#10;&lt;p&gt;Das eingebettete Video zeigt dasselbe Gravurprojekt mit vier verschiedenen Optimierungsstufen:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;oben links: Herstellerkonfiguration der Maschine, Standard-Fräspfade aus CAD/CAM&lt;/li&gt;&#10;&lt;li&gt;oben rechts: Geschwindigkeit und Beschleunigung auf Fräsbetrieb erhöht&lt;/li&gt;&#10;&lt;li&gt;unten links: Look Ahead feed eingeschaltet &lt;code&gt;G64 P0.3&lt;/code&gt;, Fräspfade im CAM optimiert&lt;/li&gt;&#10;&lt;li&gt;unten rechts: Optimiert für Gravurbetrieb wie oben beschrieben, CAM Flyover auf &lt;code&gt;2mm reduziert&lt;/code&gt; und Leerfahrten minimiert&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&lt;iframe src="https://makertube.net/videos/embed/2EWe1ZvVW9TKY6k3XtpmWF" frameborder="0" allowfullscreen&gt;&lt;/iframe&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;CNC-Tuning für den Gravurbetrieb&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>CNC Tuning #2</title><link>https://blog.schallbert.de/cnc-tuning-kinematics/</link><pubDate>Sat, 27 Apr 2024 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/cnc-tuning-kinematics/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-27-kinematics-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Thumbnail of a CNC machine&amp;#39;&amp;#39;s portal X-Axis"&#10; title="CNC Tuning #2" /&gt;&#10;&lt;p&gt;Dies ist der zweite Teil meiner Reihe über die Optimierung der Fertigungszeit auf CNC-Fräsmaschinen. Er konzentriert sich auf die kinematischen Maschinenparameter für den Gravurbetrieb.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/cnc-tuning-gcode/"&gt;&lt;em&gt;Teil 1&lt;/em&gt;: Besserer G-Code&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/cnc-tuning-pathinterpreter/"&gt;&lt;em&gt;Teil 3:&lt;/em&gt; Pfadinterpreter optimieren&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="schritt-2-tuning-der-kinematischen-parameter-der-cnc"&gt;Schritt 2: Tuning der kinematischen Parameter der CNC&lt;/h2&gt;&#10;&lt;p&gt;Die theoretischen Grundlagen habe ich im &lt;a href="https://blog.schallbert.de/portal-milling-setup/"&gt;Artikel zur CNC-Steuerungskonfiguration&lt;/a&gt; ausführlich erläutert. Meine Probleme damit und zugehörige Lösungen habe ich nach vielen Tests und Rücksprache mit dem Hersteller im &lt;a href="https://blog.schallbert.de/projects/one-year-zerspanobert/#velocityerror"&gt;Fazit zu einem Jahr CNC-Betrieb&lt;/a&gt; festgehalten. Die folgenden Abschnitte wenden die obigen Erkenntnisse auf eine Demonstrationsmaschine an, die ich mir habe leihen können.&lt;/p&gt;&#10;&lt;h3 id="verfahrgeschwindigkeit-im-eilgang"&gt;Verfahrgeschwindigkeit im Eilgang&lt;/h3&gt;&#10;&lt;p&gt;Für die Demomaschine konnte ich die Geschwindigkeit im Eilgang von den vorgeschlagenen &lt;code&gt;Fxy = 4m/min&lt;/code&gt; und &lt;code&gt;Fz = 2.4m/min&lt;/code&gt; auf &lt;code&gt;Fxy = 12m/min&lt;/code&gt; und &lt;code&gt;Fz = 4.8m/min&lt;/code&gt; erhöhen. Setze ich alle drei Achsen gleichzeitig in Bewegung, erhalte ich durch Steigung der Kugelgewindespindeln und Mikroschrittkonfiguration (8 pro Vollschritt) der Motoren eine kombinierte Schrittfrequenz von &lt;code&gt;96kHz&lt;/code&gt;, was unter der maximalen Schrittfrequenz von &lt;code&gt;125kHz&lt;/code&gt; der Steuerung liegt.&lt;/p&gt;&#10;&lt;h3 id="beschleunigung-im-eilgang"&gt;Beschleunigung im Eilgang&lt;/h3&gt;&#10;&lt;p&gt;Die Herstellerangaben empfehlen hier &lt;code&gt;aXY = 300mm/s²&lt;/code&gt; und &lt;code&gt;aZ = 200mm/s²&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Ich habe die Beschleunigung in mehreren Schritten hochgesetzt und bin in einer ersten Runde bei &lt;code&gt;aXY = 1000mm/s²&lt;/code&gt; und &lt;code&gt;aZ = 400mm/s²&lt;/code&gt; gelandet. Diese Werte sind für diese kleine und leichte Maschine mit nur &lt;code&gt;30x45x14cm&lt;/code&gt; Verfahrweg nach meiner Erfahrung für den Normalbetrieb ohne Weiteres möglich. Sie erhöhen die Fertigungsgeschwindigkeit stark, denn jeder Richtungswechsel, auch die im Materialeingriff bei &lt;code&gt;G01, G02, G03&lt;/code&gt;, werden nun um Faktor &lt;code&gt;3&lt;/code&gt; bzw. &lt;code&gt;2.5&lt;/code&gt; schneller ausgeführt.&lt;/p&gt;&#10;&lt;p&gt;So habe ich die Maschine oft betrieben und auch auf der Messe bin ich mit diesen Werten stabil und sicher gefahren. Das folgende Video zeigt die Fräse im Gravurbetrieb mit den angegebenen Werten.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&lt;iframe src="https://makertube.net/videos/embed/vvmwT8e6vWnj5zbuqMdUfN" frameborder="0" allowfullscreen&gt;&lt;/iframe&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;CNC-Tuning: Vorschau&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Danach jedoch wollte ich wissen, wie weit ich es treiben kann. Getreu dem vorher beschriebenen Motto&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Wer die Grenze nicht überschreitet, weiß nicht, wo sie liegt&amp;rdquo;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;habe ich erst einmal geprüft, wie sich massive Schrittverluste bei dieser Maschine anhören. Also habe ich eine Achse mit einem Holzbalken blockiert und bin vorsichtig dagegen gefahren:&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2024-04-27-soundoflosingsteps.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Massive step loss soundcheck&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Das deutlich vernehmbare Rattern entsteht wenn der Motorhochlauf zwar klappt, in Richtung Zielgeschwindigkeit aber diverse Vollschritte verloren gehen. Wird der Rotor bereits bei Anlauf des Motors &amp;ldquo;verloren&amp;rdquo;, so fiept der Motor nur noch, ohne die Achse überhaupt in Bewegung zu setzen. Verliert der Motor nur wenige Schritte, befindet sich aber auf Zielgeschwindigkeit, so gibt es einen rauen und unter Umständen kratzenden Ton.&lt;/p&gt;&#10;&lt;p&gt;Ich habe für das Experiment die X-Achse gewählt, weil sie sich am einfachsten blockieren lässt. Des Weiteren habe ich sie in &lt;code&gt;X+&lt;/code&gt;-Richtung blockiert. Begründung: diese Maschine besitzt kein professionelles Festlager zur Aufnahme von Druck- und Zugkräften der Kugelumlaufspindel. Stattdessen werden Druckkräfte in Richtung Schrittmotor sicher von einem auf der Spindel montierten Flansch abgefangen, der gegen in der Portalwange befindliche Kugellager drückt.&lt;/p&gt;&#10;&lt;p&gt;Zugkräfte hingegen werden hauptsächlich von den Motorlagern aufgenommen - da wollte ich kein Risiko eingehen.&#10;Nun ist also das Geräusch bei plötzlichem und ruckartigen Schrittverlust bekannt - das Übertakten kann beginnen!&lt;/p&gt;&#10;&lt;p&gt;Meine Vorgehensweise:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Die Beschleunigung einer Achse der Maschine verdoppeln&lt;/li&gt;&#10;&lt;li&gt;Die betreffende Achse umher joggen, dabei auf merkwürdige Geräusche achten und die Achsbewegung beobachten&lt;/li&gt;&#10;&lt;li&gt;Vorige Schritte wiederholen, bis massive Schrittverluste auftreten&lt;/li&gt;&#10;&lt;li&gt;Beschleunigung auf den Mittelwert zwischen &amp;ldquo;klappt&amp;rdquo; und &amp;ldquo;klappt nicht&amp;rdquo; zurückfahren&lt;/li&gt;&#10;&lt;li&gt;Den vorigen Schritt solange wiederholen, bis die Maschine sauber fährt&lt;/li&gt;&#10;&lt;li&gt;Alle Schritte auch für die anderen Achsen durchführen&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;So bin ich schließlich bei für eine solche Hobbymaschine unglaublichen &lt;code&gt;aXY = 4000mm/s²&lt;/code&gt;, &lt;code&gt;aZ = 1600mm/s²&lt;/code&gt; gelandet. Dies liegt nun eine Größenordnung über den empfohlenen Werten.&lt;/p&gt;&#10;&lt;p&gt;Die kleine Fräse scheint auch hier stabil zu laufen und die gefertigten Gravuren sehen gut aus. Allerdings konnte ich mir nicht vorstellen, dass ich Schrittmotoren, Treiberstufen und Netzteil noch innerhalb ihrer Spezifikation betreibe. Daher habe ich gemessen.&lt;/p&gt;&#10;&lt;h3 id="elektrische-messwerte"&gt;Elektrische Messwerte&lt;/h3&gt;&#10;&lt;p&gt;Das Netzteil ist mit &lt;code&gt;3.7A&lt;/code&gt; bei &lt;code&gt;36V&lt;/code&gt; angegeben. Ich darf diesen Wert also in der Summe weder im Eilgang &lt;code&gt;G00&lt;/code&gt; noch im Materialeingriff dauerhaft überschreiten. Wie oben erklärt, ist der Eilgang im Gravurbetrieb durch die geringeren Fräskräfte maßgeblich, sodass ich besonderes Augenmerk auf das Verfahren mehrerer Achsen gleichzeitig legen muss.&lt;/p&gt;&#10;&lt;p&gt;Die Motoren können je einen Dauerstrom von &lt;code&gt;3.0A&lt;/code&gt; verkraften, limitieren mir hier also nichts. In den elektrischen Tests schaue ich lediglich, ob ich mit der Energieaufnahme des Systems im Rahmen bleibe.&lt;/p&gt;&#10;&lt;h4 id="messaufbau"&gt;Messaufbau&lt;/h4&gt;&#10;&lt;p&gt;Ich schließe das aus &lt;a href="https://blog.schallbert.de/measure-inrush-current/"&gt;diesem Artikel bekannte&lt;/a&gt; Multimeter direkt am Ausgang des Netzteils an und nehme Spannung und Strom auf. Für das Experiment simuliere ich die Fertigung der oben im Video verlinkten Gravur und führe in zwei Durchläufen zuerst eine zeitlich grob aufgelöste Leistungsfaktoranalyse (&lt;code&gt;2Hz&lt;/code&gt;) durch, gefolgt von einer fein aufgelösten Stromermittlung mit &lt;code&gt;20kHz&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h4 id="ergebnisse"&gt;Ergebnisse&lt;/h4&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-27-powerdiagram-engrave.jpg" alt="Image: Power diagram for tuned engraving" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Im obigen Bild sind Spannung, Strom, Wirk- und Blindleistung aufgetragen. Zu erkennen ist, dass die Blindleistung (&lt;code&gt;grün&lt;/code&gt;) immer dann kurze Spitzen zeigt, wenn mehrere Achsen gleichzeitig in den Eilgang beschleunigen. Die Leistungsaufnahme einer Achse im Eingriff &lt;code&gt;G01/G02/G03, blau&lt;/code&gt; beträgt im Schnitt etwa &lt;code&gt;45W&lt;/code&gt; und ist ähnlich hoch wie bei der Konstantfahrt im Eilgang.&lt;/p&gt;&#10;&lt;p&gt;Das Netzteil ist in der Lage, die Spannung (&lt;code&gt;orange&lt;/code&gt;) ziemlich konstant zu halten und knickt selbst unter Volllast nur um knapp &lt;code&gt;0.2V&lt;/code&gt; ein. Einzig der Stromwert sah mir etwas zu glatt aus. Ich vermutete an dieser Stelle, dass die zeitliche Auflösung zu gering ist und beschloss, das Experiment mit Konzentration auf den Strom zu wiederholen.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-27-currentdiagram-engrave.jpg" alt="Image: Current diagram for tuned engraving" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Und tatsächlich. Die höhere zeitliche Auflösung zeigt, dass die Stromaufnahme bei Beschleunigung mehrerer Achsen Spitzen zeigt, die über den bereits bekannten Werten liegt.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-27-currentpeak-engrave.jpg" alt="Image: Zoomed-in current draw for axis acceleration" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Hier stechen Bewegungen auf der Y-Achse, welche das gesamte Portal in Bewegung setzen muss, besonders heraus. Und zwar genau dann, wenn die anderen Achsen noch in Bewegung sind oder gerade gehalten haben. Dies hängt mit den Schrittmotortreibern zusammen. Sie betreiben die Motoren nämlich noch etwa &lt;code&gt;0.5s&lt;/code&gt; nach Anhalten bei vollem Strom weiter. Die dadurch entstehende Überlagerung lässt die Spitzen noch dramatischer aussehen.&lt;/p&gt;&#10;&lt;h4 id="benchmark-spitzenlast"&gt;Benchmark Spitzenlast&lt;/h4&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-27-loadtest.jpg" alt="Image: Zoomed-in current draw for axis acceleration" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Na dann treiben wir es mal auf die Spitze mit einem Beschleunigungs-Benchmark. Wird das Netzteil auch bei voller Last unter Beschleunigung aller Motoren gleichzeitig die Ausgangsspannung halten können?&lt;/p&gt;&#10;&lt;p&gt;Um dies zu testen, habe ich einen G-Code geschrieben, der alle Achsen gleichzeitig auf Höchstgeschwindigkeit beschleunigen und dann wieder abbremsen lässt. Anschließend wird unter gleichen Bedingungen zum Ausgangspunkt zurückgefahren und das Ganze wiederholt.&lt;/p&gt;&#10;&lt;p&gt;Auf diese Weise kann ich herausfinden, ob das Netzteil genügend Reserven hat, um Lastspitzen abzufangen und gleichzeitig schauen, ob die Motoren bereits im größeren Maße Schritte verlieren. Im Mittel liegt hier die aufgebrachte Leistung bei &lt;code&gt;95W&lt;/code&gt; und die Versorgungsspannung sinkt um &lt;code&gt;0.28V&lt;/code&gt; auf &lt;code&gt;35.72V&lt;/code&gt; ab. Dabei muss das Netzteil in der Spitze &lt;code&gt;5.14A&lt;/code&gt; abgeben, was knapp &lt;code&gt;40%&lt;/code&gt; über Nennstrom liegt.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2024-04-20-benchmark.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Benchmark&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Hört man ganz genau hin, so klingt die Maschine direkt zu Beginn des Benchmarks etwas rau, fängt sich dann aber. So können auch Schrittverluste klingen. Es ist daher sehr wichtig, die Maschine mit allen Sinnen zu überwachen.&lt;/p&gt;&#10;&lt;h3 id="mechanische-prüfung"&gt;Mechanische Prüfung&lt;/h3&gt;&#10;&lt;p&gt;Nun zur letzten Prüfung: Ich gebe beim Eilgang-Benchmark zusätzlich Last auf die Achsen, indem ich während des Programmablaufes mit meinem Arm an der jeweiligen Achse ziehe. Die Achsen Z und X laufen stabil weiter. Bei der Portalachse &lt;code&gt;Y&lt;/code&gt; merke ich jedoch, dass die Maschine nun anders klingt. Irgendwie rau, als würde man mit mit einer Raspel leicht über eine Holzplatte ziehen.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-27-steplossy.jpg" alt="Image: Y-axis reveals massive step loss" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Lege ich eine große Last an, so fährt der Schrittmotor auf dieser Achse nicht mal mehr an, sondern fiept nur noch. Er kann den Pulsen der Endstufe nicht mehr folgen. Ich reduziere nun also die Beschleunigung schrittweise, bis der Motor die Portalachse auch unter mittlerer Last wiederholbar und ohne hörbare Schrittverluste beschleunigen kann. Hier lande ich am Ende bei immer noch flinken &lt;code&gt;[aX = 4000mm/s² aY = 3200mm/s² aZ = 1600mm/s²]&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;In einem letzten Schritt führe ich nach fünfmaliger Wiederholung des Benchmarks eine weitere Referenzfahrt durch und schaue mir die Logs der Steuersoftware an. Alle Achsen sind nun unauffällig hinsichtlich Schrittverlusten.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-27-steplossxz.jpg" alt="Image: XZ-axis without step loss" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h3 id="appelle-zum-cnc-tuning"&gt;Appelle zum CNC-Tuning&lt;/h3&gt;&#10;&lt;p&gt;Bitte geht beim Tuning eurer Maschine behutsam vor. Lernt sie richtig kennen, bevor ihr damit beginnt. Hört genau hin und optimiert Achse für Achse nacheinander. Nehmt euch Zeit für Tests, Prüfungen und Experimente.&lt;/p&gt;&#10;&lt;p&gt;Selbst wenn ihr eine sehr ähnliche Maschine wie meine besitzt, machen andere Abmessungen bereits einen riesigen Unterschied hinsichtlich Steifigkeit und damit auch möglicher Verfahrgeschwindigkeit bzw. Beschleunigung. Sogar Wartungszustand, leichtgängigere Wellen, andere Führungen o.Ä. können dazu führen, dass ihr am Ende mit anderen Werten fahrt als ich.&lt;/p&gt;&#10;&lt;p&gt;Es wird kein Profi-Equipment benötigt, um die Grenzen der Maschine auszuloten. Nicht einmal eine Messuhr, da Schrittverluste auch mit Hilfe der Referenzschalter hinreichend genau bestimmt werden können.&lt;/p&gt;&#10;&lt;p&gt;Alles was ich durch die elektrischen Messungen oben festgestellt habe, ist, dass der Hersteller bei der Auslegung der Maschine sauber gearbeitet hat und keine Komponente unterdimensioniert ist. Im Gegenteil, diese Maschine ist für die Übertaktung bestens geeignet.&lt;/p&gt;&#10;&lt;p&gt;Und nun weiter zum letzten &lt;a href="https://blog.schallbert.de/cnc-tuning-pathinterpreter/"&gt;&lt;em&gt;Teil 3:&lt;/em&gt; Pfadinterpreter optimieren&lt;/a&gt;.&lt;/p&gt;&#10;</description></item><item><title>CNC Tuning #1</title><link>https://blog.schallbert.de/cnc-tuning-gcode/</link><pubDate>Sat, 20 Apr 2024 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/cnc-tuning-gcode/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-20-camtuned-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Thumbnail of CAM simulation for G-code optimizing"&#10; title="CNC Tuning #1" /&gt;&#10;&lt;p&gt;Dies ist der erste Teil meiner Reihe über die Optimierung der Fertigungszeit auf CNC-Fräsmaschinen. Er konzentriert sich auf den Einsatz von CAD- und CAM-Werkzeugen zur Erzeugung effizienteren G-Codes.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/cnc-tuning-kinematics/"&gt;&lt;em&gt;Teil 2&lt;/em&gt;: Kinematische Parameter hochdrehen&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/cnc-tuning-pathinterpreter/"&gt;&lt;em&gt;Teil 3:&lt;/em&gt; Pfadinterpreter optimieren&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Es gibt diverse Maßnahmen zur Reduktion der Fertigungszeit mit einer CNC-Maschine. Erstmal jedoch sollte beleuchtet werden, warum so etwas gewünscht ist und welche möglichen Nachteile sich hierdurch ergeben.&lt;/p&gt;&#10;&lt;h2 id="warum-überhaupt"&gt;Warum überhaupt?&lt;/h2&gt;&#10;&lt;p&gt;Aus kaufmännischer Sicht sorgen geringere Fertigungszeiten für einen höheren Teiledurchsatz und ermöglichen damit einen höheren Umsatz in einer gegebenen Zeitspanne. Zudem ist die Investition der Maschine schneller amortisiert.&lt;/p&gt;&#10;&lt;p&gt;Als Hobbyist sind kürzere Fertigungszeiten vor allem dann interessant, wenn das Ziel die Herstellung sehr komplexer Geometrien oder die 3D-Fertigung ist. Hier können Fräszeiten durchaus mehrere Stunden betragen und die möglichen Einsparungen sind folglich besonders groß.&lt;/p&gt;&#10;&lt;p&gt;Bei mir steht wie auch in der Lehre der wissenschaftliche Gedanke im Vordergrund: Ich will herauszufinden, wo die Grenze der Maschine liegt. Ich möchte wissen, warum sie dort liegt. Ich werde versuchen, sie stabil innerhalb ihrer Grenzen zu betreiben. Dabei ist der Verschleiß unter Kontrolle zu halten.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;Um diese Grenze jedoch genau zu kennen, muss sie zuerst übertreten werden. Woher sonst soll ich wissen, wo sie sich befindet?&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h3 id="was-sind-mögliche-nachteile"&gt;Was sind mögliche Nachteile?&lt;/h3&gt;&#10;&lt;p&gt;Bestimmte Komponenten einer &amp;ldquo;übertakteten&amp;rdquo; Maschine könnten schneller als üblich verschleißen. Dazu zählen etwa Netzteile und Motor-Endstufen, die höhere transiente Ströme verkraften müssen. Lager und Führungen werden durch höhere Beschleunigungen stärker beansprucht. Umlaufspindeln und Maschinenrahmen müssen die dadurch resultierenden, stärkeren und schneller wechselnden Kräfte und Vibrationen aufnehmen und können früher Ermüdungserscheinungen oder sich lösende Verbindungen aufweisen.&lt;/p&gt;&#10;&lt;p&gt;Bei der Ermittlung der Grenzen kann die Maschine im Extremfall sogar beschädigt werden: Mit hoher Wahrscheinlichkeit treten jenseits der Grenze Schrittverluste&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; auf. Das ist zwar im Testbetrieb nicht weiter problematisch. Doch stimmt die angenommene Position dann nicht mehr mit der tatsächlichen Position überein und es besteht das Risiko des &amp;ldquo;Crashes&amp;rdquo; - wenn nicht mit Bedacht vorgegangen wird. Hier noch ein ziemlich gutes &lt;a href="https://www.youtube.com/watch?v=C_9dM1wx0Sg" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Youtube-Video von &lt;em&gt;gammaflow&lt;/em&gt; zum Thema Schrittverluste vermeiden&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Werden die Fräsbahnen im CAD oder CAM zugunsten kürzerer Fertigungszeiten verändert, so kann dies außerdem negative Auswirkungen auf Schnitt-, Kanten- und Oberflächenqualität haben.&lt;/p&gt;&#10;&lt;p&gt;Bei der Optimierung des Pfadinterpreters der Steuersoftware ist ebenfalls ein Kompromiss einzugehen: Höhere Fertigungsgeschwindigkeit wird hier mit geringerer Genauigkeit und/oder höherem Maschinenverschleiß eingekauft.&lt;/p&gt;&#10;&lt;h3 id="welche-maßnahmen-werden-nicht-diskutiert"&gt;Welche Maßnahmen werden nicht diskutiert?&lt;/h3&gt;&#10;&lt;p&gt;Nicht betrachten werde ich hier die Optimierung von Vorschub &lt;code&gt;F&lt;/code&gt; und Drehzahl &lt;code&gt;S&lt;/code&gt; des gewählten Fräsers im Eingriff. Diese Parameter hängen vom zu bearbeitendem Material, der Fräsergeometrie, den Fähigkeiten der Maschine und dem Mut des Bedienpersonals ab, sodass ich keine allgemein gültigen Tunings vorschlagen kann.&lt;/p&gt;&#10;&lt;p&gt;Einen anderen, großen Einflussfaktor muss ich hier ebenfalls ausklammern: Die Bearbeitungsstrategie des CAM-Programms bezogen auf Eilgänge. Mein CAM-Programm lässt mich da nicht besonders tief eingreifen, sodass ich für die Optimierung der letzten paar Prozent Fertigungszeit stets von Hand im G-Code arbeiten muss, was entsprechend fehleranfällig ist.&lt;/p&gt;&#10;&lt;h2 id="tuning-in-drei-schritten"&gt;Tuning in drei Schritten&lt;/h2&gt;&#10;&lt;p&gt;Bei meiner Optimierung bin ich in drei Schritten vorgegangen, die allerdings unabhängig voneinander umgesetzt werden können. Jeder Schritt hat mir hierbei deutlich spürbare Verbesserungen gebracht. Dieser Artikel konzentriert sich aus mehreren Gründen auf den Gravurbetrieb:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Gravuren erlauben meist größere Toleranzen als andere Betriebsarten. Dadurch kann stärker verrundet werden, was zu geringeren Beschleunigungen führt. Dies ermöglicht im Mittel größere Vorschübe&lt;/li&gt;&#10;&lt;li&gt;Die zu erwartenden Fräskräfte sind geringer: Keine Durchfräsungen, kaum Energieeinsatz bei der Spanverdrängung, kleineres Spanvolumen als bei Schaftfräsern&lt;/li&gt;&#10;&lt;li&gt;Dadurch kann die Maschine näher an ihren mechanischen und elektrischen Grenzen betrieben werden&lt;/li&gt;&#10;&lt;li&gt;Durch im Schnitt größere G-Code Dateien mehr Optimierungspotential&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="schritt-1-besserer-g-code"&gt;Schritt 1: Besserer G-Code&lt;/h2&gt;&#10;&lt;p&gt;Dieser Abschnitt folgt dem Credo &lt;code&gt;Garbage in, Garbage out&lt;/code&gt;. Will heißen: Wenn mein G-Code schon schlecht ist, kann die Maschine niemals optimale Fertigungszeiten erreichen. Was zeichnet also &amp;ldquo;schlechten&amp;rdquo; G-Code in Bezug auf Fertigungszeit aus?&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;übermäßig komplex und lang, möglicherweise Dateigröße im Megabyte-Bereich&lt;/li&gt;&#10;&lt;li&gt;extrem viele Stützpunkte und eine Menge kurzer Liniensegmente&lt;/li&gt;&#10;&lt;li&gt;Einen Haufen &amp;ldquo;Leerfahrten&amp;rdquo; im Eilgang auf XY mit hochgezogener Z-Achse&lt;/li&gt;&#10;&lt;li&gt;Viele Rückzüge der Z-Achse auf Sicherheitshöhe&lt;/li&gt;&#10;&lt;li&gt;Häufige Wartezeiten der Maschine (Spindelhochlauf, Kühlmittelpumpe, Controller-Puffer voll)&lt;/li&gt;&#10;&lt;li&gt;Übermäßiges Feinstschlichten auch bei B-Oberflächen&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="aber-wie-soll-ich-meinen-g-code-vereinfachen"&gt;Aber wie soll ich meinen G-Code vereinfachen?&lt;/h3&gt;&#10;&lt;p&gt;Kurze Frage, lange Antwort: Es fängt im CAD an und hört möglicherweise im CAM nicht auf.&lt;/p&gt;&#10;&lt;h3 id="beispiel-cad"&gt;Beispiel CAD&lt;/h3&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-20-bline.jpg" alt="Image: Letter &amp;#39;B&amp;#39; in CAM, drawn as a set of line segments" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Nehmen wir an, wir möchten eine Schrift fräsen. Unser CAD-Programm hat auch einen Haufen toller Schriftarten dazu. Wir erzeugen nun eine DXF-Datei für&amp;rsquo;s CAM. Im CAM dann sehen wir plötzlich beim Hineinzoomen, dass alle Buchstaben aus sehr kurzen Liniensegmenten bestehen statt aus Kreissegmenten oder &lt;a href="https://en.wikipedia.org/wiki/Spline_%28mathematics%29" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Splines&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Die Folge: Das CAM wird &lt;code&gt;G01&lt;/code&gt;-Bewegungen, also &amp;ldquo;Linear Moves&amp;rdquo; bzw. eine Aneinanderreihung von Strecken erzeugen.&lt;/p&gt;&#10;&lt;p&gt;Die verbundenen Strecken machen sehr viele Richtungswechsel der Maschine erforderlich. Folglich muss sie vor dem Verbindungspunkt ständig abbremsen und danach wieder beschleunigen. Die Maschine kann also nicht mit konstanter Geschwindigkeit fahren, denn das würde eine unendliche Beschleunigung am Knickpunkt erfordern.&lt;/p&gt;&#10;&lt;p&gt;Für dieses Problem gibt es Lösungen, die im Idealfall gemeinsam angewendet werden:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Im CAD-Programm einstellen, dass wann immer möglich Splines oder Kurven statt Liniensegmenten erstellt werden sollen&lt;/li&gt;&#10;&lt;li&gt;Vereinfachung der Zeichnung durch Reduktion der Stützstellen&lt;/li&gt;&#10;&lt;li&gt;Vermehrte Nutzung von Radien und Splines an Kanten, Vermeidung von Winkeln&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;In meinem Falle war die erste Option nicht verfügbar, sodass ich nun ein anderes Programm für die Gravur von Schriften verwende als für meine üblichen Zeichnungen. Dadurch kann ich Schrift jetzt in Linien- und Bogensegmenten ausdrücken. Die dafür erforderliche Fräszeit halbiert sich und gleichzeitig kommt der G-Code mit weniger Zeilen aus. Außerdem wird die Schrift runder und hält sich sogar noch genauer an die anfangs gewählte Schriftart.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-20-bsplinevsline.jpg" alt="Image: Letter &amp;#39;B&amp;#39; in CAM, twice. Once per spline, once as line segments" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Gute Beispiele für die Vereinfachung von Pfaden durch Reduktion von Stützpunkten liefern Gimp und Inkscape - für Inkscape habe ich das sogar bereits meinem &lt;a href="https://blog.schallbert.de/path-from-image/#inkscape"&gt;Artikel zum Thema Bild-zu-Pfad-Konversion&lt;/a&gt; gezeigt. Mit ein paar wenigen Klicks erhält man auf Kosten der Genauigkeit Zeichnungsdaten, die teils deutlich schneller zu fertigen sind.&lt;/p&gt;&#10;&lt;p&gt;Da ein Schaftfräser sowieso keine Tasche mit &lt;code&gt;r=0&lt;/code&gt; in den Ecken fertigen kann, habe ich mir angewöhnt, sämtliche Ecken und Kanten im CAD abzurunden. Nicht nur liegen so hergestellte Teile besser in der Hand, sie sind außerdem schneller zu fertigen - die Zeitersparnis summiert sich, besonders auf langsamen und trägen Maschinen.&lt;/p&gt;&#10;&lt;h3 id="beispiel-cam"&gt;Beispiel CAM&lt;/h3&gt;&#10;&lt;p&gt;Im CAM kann ich durch vermehrte Verwendung von Anfahrpunkten und Rampen ein wenig Zeit sparen. Ich lege sie zumeist an das kürzeste Liniensegment und spare so einen Brems/Beschleunigungsvorgang. Bei der Eintauchart ist der Vorteil von &amp;ldquo;Rampe&amp;rdquo; gegenüber &amp;ldquo;Helix&amp;rdquo;, dass sämtliche Wege des Fräsers Nutzwege sind und ich ohne zusätzliche Achsbeschleunigungen durchfahren kann. Je nach Rampensteigung allerdings verlängert sich der Fräspfad, da Überlappungen entstehen.&lt;/p&gt;&#10;&lt;p&gt;Einen viel größeren Effekt auf die Fertigungszeit hat die kluge Auswahl von Räumzustellungen und -strategien für Taschen passend zu Material, Fräser und gewünschter Oberflächengüte. So erzeugt Wirbelfräsen (Trochoidalfräsen&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;) in vielen Fällen längere Dateien und kostet auf der Maschine mehr Zeit als der klassische &amp;ldquo;Rasenmäher&amp;rdquo;, welcher die Tasche bloß abzeilt. Für die Räumzustellung wähle ich bei Materialien wie Holz und Acrylglas gern große Werte über &lt;code&gt;60%&lt;/code&gt; - es sei denn, der Boden der Tasche muss besonders hohen Qualitätsansprüchen genügen.&lt;/p&gt;&#10;&lt;p&gt;Für Gravuren ist es meiner Meinung nach oft am Schnellsten, auf einen Räumgang mit einem zweiten Fräser zu verzichten und wenn möglich einen Gravurfräser mit abgeflachter Spitze - ähnlich derer für meine &lt;a href="https://blog.schallbert.de/cut-dibond-tests/"&gt;Tests mit Dibond&lt;/a&gt; - zu verwenden. Dieser benötigt zum Räumen zwar länger; das kann ich durch gesteigerte Gravurtiefe und eine recht breite Abflachung von &lt;code&gt;1.5mm&lt;/code&gt; aber kompensieren. Ich verwende bei einem solchen Fräser eine Räumzustellung in XY von &lt;code&gt;1mm&lt;/code&gt;. Übrig bleibt schließlich der Zeitvorteil durch weniger Werkzeugwechsel.&lt;/p&gt;&#10;&lt;h3 id="aber-wie-soll-ich-die-leerfahrten-reduzieren"&gt;Aber wie soll ich die Leerfahrten reduzieren?&lt;/h3&gt;&#10;&lt;p&gt;Endlich mal ein paar knackig-kurze Antworten:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Geringere Rückzugshöhe in &lt;code&gt;Z&lt;/code&gt; (ich verwende &lt;code&gt;5mm&lt;/code&gt; aber &lt;code&gt;2mm&lt;/code&gt; würde bei mir wahrscheinlich auch funktionieren)&lt;/li&gt;&#10;&lt;li&gt;Optimierung der Frässtrategie im CAM auf &amp;ldquo;minimale Wege&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;Bei Nesting: Darauf achten, dass die Geometrie möglichst nicht Zeile für Zeile, sondern Teil für Teil gefertigt wird.&lt;/li&gt;&#10;&lt;li&gt;Im Zweifel Optimierung der Strategie im G-Code von Hand. Lohnt sich dann, wenn das CAM-Programm nicht gut genug arbeiten kann und viele Gleichteile zu fertigen sind&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="und-wo-ist-das-problem-mit-rückzügen-der-z-achse"&gt;Und wo ist das Problem mit Rückzügen der Z-Achse?&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Rückzüge finden vor allem im Gravurbetrieb sehr häufig statt&lt;/li&gt;&#10;&lt;li&gt;der Zeitverlust summiert sich&lt;/li&gt;&#10;&lt;li&gt;Beim Wiedereingriff wird meist nicht im Eilgang sondern mit für den Fräser vorgesehener Geschwindigkeit mit &lt;code&gt;G01&lt;/code&gt; auf das Material gefahren&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Eine Verbesserung kann hier eine gute Planung der Fräsreihenfolge im CAM bringen. Möglichst viele Sektionen sollten verbunden und ohne Absetzen des Fräsers gefertigt werden. Im CAM sollte für Rückzug und Wiedereinfahrt in &lt;code&gt;Z&lt;/code&gt; der Eilgang verwendet werden.&lt;/p&gt;&#10;&lt;h3 id="wie-reduziere-ich-wartezeiten"&gt;Wie reduziere ich Wartezeiten?&lt;/h3&gt;&#10;&lt;p&gt;Mit Kühlschmierstoffschaltvorgängen, Wechsel der Spindeldrehzahl oder des Werkzeugs sind Wartezeiten verbunden, die meist leicht zu reduzieren sind:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Kühlschmierstoffförderung durchlaufen lassen während des Programmablaufes&lt;/li&gt;&#10;&lt;li&gt;die Verwendung möglichst weniger Werkzeuge bei gleichbleibender Drehzahl für ein Produkt&lt;/li&gt;&#10;&lt;li&gt;Anschläge verwenden, um die Maschinenbeschickung zu vereinfachen und Nullpunkte nicht immer wieder neu setzen zu müssen&lt;/li&gt;&#10;&lt;li&gt;Wird die Maschine mit komplett verschiedenen parametern für Gravur- und Fräsbetrieb gefahren, empfiehlt sich die doppelte Installation der Steuersoftware mit jeweils passenden Parametersätzen. Dadurch entfällt das ständige Ändern der Konfiguration.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Weiter geht es in &lt;a href="https://blog.schallbert.de/cnc-tuning-kinematics/"&gt;&lt;em&gt;Teil 2&lt;/em&gt;: Kinematische Parameter hochdrehen&lt;/a&gt;.&lt;/p&gt;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;Schrittverluste treten dann auf, wenn die Achse eines Schrittmotors nicht mehr synchron mit der durch die Steuerung vorgegebenen Schrittfolge dreht, sondern weniger bzw. mehr Schritte zurücklegt als vorgegeben. Fast immer ist die Ursache eines Schrittverlustes in einer unzureichenden Auslegung des Systems (z.B. zu geringe oder instabile Versorgungsspannung, zu schwache oder nicht an den Motor angepasste Endstufen) oder einer Überlastung des Schrittmotors zu finden. Selten jedoch können Schrittverluste Symptome von Defekten sein wie Lagerfraß, Windungsschluss oder durchschlagene Pufferkondensatoren.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:2"&gt;&#10;&lt;p&gt;Das Trochoidalfräsen ist keine schlechte Praxis. Es hat andere Stärken, ist im Gravurrbetrieb aber nur selten von Nutzen.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>Hardware Test</title><link>https://blog.schallbert.de/testing-hardware/</link><pubDate>Sun, 23 Apr 2023 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/testing-hardware/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-04-23_subsystem_test-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Subsystem boundary diagram"&#10; title="Hardware Test" /&gt;&#10;&lt;h2 id="ein-neuer-antrieb"&gt;Ein neuer Antrieb&lt;/h2&gt;&#10;&lt;p&gt;Kürzlich habe ich Zerspanobert ein &lt;a href="https://blog.schallbert.de/projects/spindle-upgrade/"&gt;Spindel-Upgrade&lt;/a&gt; gegönnt. Ich bin jetzt auf eine Drehstrom-Asynchronmaschine umgestiegen, welche mit einem 24V-Lüfter fremdgekühlt wird. Für den Betrieb im benötigten Drehzahlbereich musste ich in einen Frequenzumrichter investieren, der in meinem Falle wie beim vorigen Fräsmotor auch über ein analoges &lt;code&gt;0-10V&lt;/code&gt; Signal gesteuert wird. Aus Sicht der CNC ist die neue Spindel bis auf eine Ausnahme identisch mit der alten: Der Frequenzumrichter kann nun ein &amp;ldquo;Not-Aus&amp;rdquo; Signal erzeugen und an die Steuerung melden, z.B. wenn die Drehzahl ungeplant abfällt, zu lange ein Überlastzustand auftritt etc.&lt;/p&gt;&#10;&lt;p&gt;Wie man sich vorstellen kann ist das neue Subsystem rund um die Spindel deutlich komplizierter als das alte.&lt;/p&gt;&#10;&lt;h2 id="warum-extra-tests"&gt;Warum extra Tests?&lt;/h2&gt;&#10;&lt;p&gt;Mit höherer Systemkomplexität erhöht sich auch das Risiko für Fehlfunktionen. Es sind eine Menge mehr elektrischer, elektronischer und mechanischer Bauteile verbaut als bei meinem Fräsmotor, die selbst Defekte aufweisen oder bei falscher Verdrahtung selbst in Rauch aufgehen können. Da ich als Systemdesigner selbst nur ein Mensch bin der Fehler macht, wäre es nicht besonders schlau, das System irgendwie zusammenzustecken und dann direkt auszuprobieren.&lt;/p&gt;&#10;&lt;p&gt;Während ich die Verdrahtung erstellte, überlegte ich mir also, wie ich das System effizient testen könne, ohne etwas zu zerstören.&lt;/p&gt;&#10;&lt;h2 id="teststrategie"&gt;Teststrategie&lt;/h2&gt;&#10;&lt;p&gt;Ich habe die unterschiedlichen Testbereiche von meiner Arbeit in der Softwareindustrie geborgt. Sie passen hier denke ich recht gut hinein:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Unit&lt;/strong&gt;: Ein Test, der das Verhalten einer Komponente des Systems isoliert überprüft. Alle externen Einflüsse wurden entweder entfernt oder durch Komponenten ersetzt, auf die im Test selbst problemlos zugegriffen werden kann. Beispiel: Prüfe, ob der Lüfter bei Anlegen seiner Betriebsspannung wie erwartet im richtigen Drehsinn anläuft.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Integration&lt;/strong&gt;: Diese Tests prüfen das Zusammenspiel verschiedener Komponenten, welche gemeinsam eine Funktion abbilden. Beispiel: Überprüfe, ob die Luftkühlung der Spindel auch nach Abschalten derselben für eine bestimmte Zeit nachläuft.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Subsystem&lt;/strong&gt;: Verifiziert die korrekte Interaktion aller Komponenten eines Subsystems. Beispiel: Die Spindeldrehzahl kann über eine analoge Spannung am Eingang des Frequenzumrichters verändert werden.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;System (End2End)&lt;/strong&gt;: Diese Tests werden aus Kundensicht ausgeführt und durchlaufen das komplette System. Beispiel: Wenn ich den Not-Aus an der CNC betätige, muss der Frequenzumrichter die Spindel umgehend sicher zum Stillstand bringen.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-04-23_test_scopes.jpg" alt="Image: different test scopes of my CNC system" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h3 id="teste-früh"&gt;Teste früh&lt;/h3&gt;&#10;&lt;p&gt;Aus meiner Erfahrung macht es Sinn, die Tests zum frühestmöglichen Zeitpunkt durchzuführen. Nachdem die Spindel geliefert worden war, konnte ich auf diese Weise sofort nachvollziehen, ob alle Lager leichtgängig sind, ob der Lüfter funktioniert und ob der elektrische Widerstand der Statorwicklungen im erwarteten Bereich liegt. Alle diese Tests waren durchführbar noch bevor ich mit dem Bau des Schaltschranks überhaupt begonnen hatte.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2023-04-23-blowertest.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Unit test: Lüfter&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Die schnell und einfach durchführbaren Unit Tests können einen Haufen Zeit sparen, denn diese Fehlerquellen kann man später bei der Inbetriebnahme direkt ausschließen. Und findet man doch etwas so früh, kann man sich direkt um Ersatz kümmern und hält nicht den Einrichtbetrieb auf.&lt;/p&gt;&#10;&lt;h3 id="teste-von-unten-nach-oben"&gt;Teste &amp;ldquo;von unten nach oben&amp;rdquo;&lt;/h3&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-04-23_test_pyramid.jpg" alt="Image: my test pyramid" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Bei den vier Testebenen versuche ich, so viel wie möglich mit einfachen und schnellen Unit Tests abzudecken. Was hier noch nicht geht, wird jeweils auf der nächsthöheren Ebene abgearbeitet und so weiter. Am Ende möchte ich nur ein paar Wenige Tests aus Kundensicht durchführen, da diese meist einen größeren Aufwand für Vor- und Nachbereitung benötigen und außerdem zumeist nicht genau in der Lage sind, den Fehler zu lokalisieren.&lt;/p&gt;&#10;&lt;p&gt;Dies deckt sich mit dem Konzept der &lt;a href="https://martinfowler.com/testing/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Testpyramide&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, welche in der Softwareentwicklung des Öfteren herangezogen wird.&lt;/p&gt;&#10;&lt;h2 id="test-plan"&gt;Testplan&lt;/h2&gt;&#10;&lt;p&gt;Für meine privaten Hobbyprojekte schreibe ich üblicherweise keinen Testplan. Meine Systeme sind meist deutlich einfacher als dieses. Außerdem benötigen nur die wenigsten potentiell gefährliche Niederspannung wie in diesem Fall.&lt;/p&gt;&#10;&lt;p&gt;Um das System besser zu visualisieren habe ich mal einen groben Schaltplan mit allen Komponenten sowie relevanten Ein- und Ausgängen gezeichnet.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-04-23_system_layout.jpg" alt="Image: motor spindle subsystem layout" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Von diesem Bild habe ich einen Testplan abgeleitet. Hierbei habe ich versucht, die Interaktionen jeder Komponente mit jeder anderen abzudecken und wenn möglich versucht, dem Komponententest den Vorrang zu geben.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-04-23_test_plan.jpg" alt="Image: A test plan for the motor spindle subsystem" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2023-04-23-powersupplytest.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Unit test: Stromversorgung &amp;#43;24V OK&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Hier überprüfe ich die Versorgungsspannung für den 24V-Lüfter&lt;/p&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2023-04-23-relaytest.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Unit test: Abfallverzögerung&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Mit diesem Test stelle ich sicher, dass die Konfiguration der Anzugs- und Abfallverzögerung wie gewünscht funktioniert. Für den Test habe ich kürzere Zeiten gewählt als später im produktiven Betrieb.&lt;/p&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2023-04-23-vfdtest.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Integrationstest: FU Inbetriebnahme&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Hier prüfe ich, ob der Frequenzumrichter die Spindel mit der korrekten Drehrichtung und mit unterschiedlichen Drehzahlen betreiben kann.&lt;/p&gt;&#10;&lt;h2 id="ausführung-der-tests"&gt;Ausführung der Tests&lt;/h2&gt;&#10;&lt;p&gt;Es gibt natürlich noch viele weitere hardwarebasierte Tests die ich oben nicht erwähnt habe. Dinge wie &amp;ldquo;ist die Spindel korrekt mit der Z-Achse ausgerichtet?&amp;rdquo; &amp;ldquo;Habe ich alle Schrauben mit dem vorgeschriebenen Drehmoment angezogen?&amp;rdquo; &amp;ldquo;Sind alle Stecker angeschlossen und verriegelt?&amp;rdquo;&lt;/p&gt;&#10;&lt;p&gt;Bevor ich nach dem Umbau also das erste mal wieder Späne mache, habe ich den Testplan komplett abgearbeitet und tatsächlich ein Problem auf Systemebene entdeckt: Ich habe den Analogausgang zur Vorgabe der Spindeldrehzahl falsch verdrahtet, sodass andere Pins das Signal führten als geplant. Dies führte dazu, dass sich die Spindel trotz Freigabe nicht starten ließ, obwohl die CNC &amp;ldquo;Spindel läuft&amp;rdquo; ausgab.&lt;/p&gt;&#10;&lt;p&gt;Dank der vorausgehenden Tests am Frequenzumrichter (&amp;ldquo;Reacts to speed command&amp;rdquo;) habe ich nur Minuten benötigt um den Fehler zu finden und zu beheben - denn ich wusste, am FU liegt&amp;rsquo;s nicht.&lt;/p&gt;&#10;&lt;h2 id="dokumente"&gt;Dokumente&lt;/h2&gt;&#10;&lt;p&gt;Falls Sie planen, eine Frequenzumrichtergesteuerte Motorspindel an Ihrer Maschine einzusetzen, können Sie sich meinen Testplan kostenlos herunterladen.&#10;In case you plan on adding a VFD-controlled spindle motor to your machine, please find my full test plan below.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://blog.schallbert.de/assets/docs/SpindleTests.pdf"&gt;VFD-controlled spindle motor test plan&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;Aber bitte betrachten Sie diesen Plan mehr als Unterstützung denn als vollumfängliche Prüfung und beachten Sie vor allem die üblichen Warnhinweise:&lt;/p&gt;&#10;&lt;p&gt;⚠️ &lt;strong&gt;Warnung vor gefährlicher Spannung&lt;/strong&gt; ⚠️&lt;/p&gt;&#10;&lt;p&gt;Dieses System sollte nur von Profis konstruiert, zusammengebaut, angepasst, in Betrieb genommen und schließlich fachgerecht geprüft werden. In diesem Falle meine ich mit &amp;ldquo;Profi&amp;rdquo; jemanden, der eine abgeschlossene Berufsausbildung zum Elektriker oder Elektroniker, optimalerweise in der Fachrichtung Anlagenbau/Betriebstechnik sowie Erfahrung im Umgang mit Industrieelektronik vorzuweisen hat. Vermeiden Sie insbesondere, unter Spannung stehende Teile zu berühren oder an einer der Komponenten zu arbeiten, bevor der Frequenzumrichter nach dem Abschalten genügend Zeit hatte, seine Pufferkondensatoren zu entladen.&lt;/p&gt;&#10;</description></item><item><title>GoogleTest Demo</title><link>https://blog.schallbert.de/projects/platformio_gtestgmock/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/platformio_gtestgmock/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/platformio_gtestgmock/build_test_success-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Googletest&amp;#39;s successful test results on the example program"&#10; title="GoogleTest Demo" /&gt;&#10;&lt;h2 id="projekt-steckbrief"&gt;Projekt-Steckbrief&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Schwierigkeitsgrad: Mittel 3/5&lt;/li&gt;&#10;&lt;li&gt;Kosten: 0€&lt;/li&gt;&#10;&lt;li&gt;Zeitaufwand: ~2h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;&#10;&lt;p&gt;Es ist schön, bei der Entwicklung komplexerer Projekte ein leistungsfähiges Unit-Test-Framework zur Hand zu haben, das viel Zeit bei der Fehlersuche spart (vor allem das On-Target-Debugging in der Embedded-Welt kann sehr zeitaufwändig sein), die Codequalität verbessert und möglicherweise den Einblick in APIs und Architektur für andere verbessert.&lt;/p&gt;&#10;&lt;h2 id="die-welt-der-embedded-controller"&gt;Die Welt der &amp;ldquo;embedded controller&amp;rdquo;&lt;/h2&gt;&#10;&lt;p&gt;Traditionell waren Mikrocontroller auf der Speicherseite begrenzt. In der Vergangenheit hatten sie weder viel RAM noch PROGMEM, also war mein meist in schlankem &lt;code&gt;C&lt;/code&gt; geschrieben und die Konfiguration fand in Listen von &lt;code&gt;#define&lt;/code&gt; statt, die nicht viel Rechenzeit kosten, weil der Präprozessor die Arbeit machen würde und nicht der µC zur Laufzeit.&#10;Meine Unit-Tests waren immer an die Hardware gebunden, auf der sie liefen - nehmen Sie &lt;a href="https://github.com/ThrowTheSwitch/Unity" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Unity&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; als bekanntes Beispiel - aber jetzt wollte ich etwas Neues ausprobieren:&#10;Code objektorientiert schreiben und die Geschäftslogik unabhängig von der Hardware testen lassen. Ich wählte das &lt;a href="http://google.github.io/googletest" target="_blank" rel="noopener noreferrer" class="external-link"&gt;GoogleTest&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;-Framework und wollte eine nahtlose Integration in meinen Workflow mit &lt;a href="https://code.visualstudio.com" target="_blank" rel="noopener noreferrer" class="external-link"&gt;VScode&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und dessen Erweiterung &lt;a href="https://platformio.org" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PlatformIO&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; erreichen.&lt;/p&gt;&#10;&lt;p&gt;Hierfür blieb bis vor kurzem nur der Weg, &lt;em&gt;GoogleTest&lt;/em&gt; per &lt;em&gt;GNU Compiler Collection&lt;/em&gt; parallel zu betreiben und durch PlatformIO ansteuern zu lassen. Neuerdings allerdings lässt sich &lt;em&gt;GoogleTest&lt;/em&gt; als &lt;a href="https://registry.platformio.org/libraries/google/googletest" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PlatformIO Library&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; direkt nutzen. Daher gehe ich hier auf beide Wege ein.&lt;/p&gt;&#10;&lt;h2 id="googletest-direkt-in-platformio-verwenden"&gt;GoogleTest direkt in PlatformIO verwenden&lt;/h2&gt;&#10;&lt;p&gt;Hier gestaltet sich die Installation besonders einfach:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Mein Github-Repository &lt;a href="https://github.com/Schallbert/PlatformIO_gTestgMock/tree/googletest-native" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PlatformIO-gTestgMock&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; klonen.&lt;/li&gt;&#10;&lt;li&gt;Im Repository-Ordner ein Konsolenfenster öffnen und per &lt;code&gt;git checkout googletest-native&lt;/code&gt; auf den entsprechenden Branch wechseln.&lt;/li&gt;&#10;&lt;li&gt;Den Anweisungen in der &lt;em&gt;README&lt;/em&gt;-Datei des Repository folgen und das Setup ausprobieren.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;So sieht es aus, wenn die Tests erfolgreich durchgeführt wurden:&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/platformio_gtestgmock/native_test.jpg" alt="Image: googletest result success message in PlatformIO&amp;#39;s native environment" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&lt;/p&gt;&#10;&lt;h2 id="googletest-per-gcc-einsetzen"&gt;GoogleTest per GCC einsetzen&lt;/h2&gt;&#10;&lt;p&gt;Die benötigte Software kann man selbst (auch auf einem Windows&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; Rechner) installieren, um mit GoogleTest starten zu können.&lt;/p&gt;&#10;&lt;h3 id="mingw-herunterladen-und-installieren"&gt;MinGW herunterladen und installieren&lt;/h3&gt;&#10;&lt;p&gt;Siehe &lt;a href="https://community.platformio.org/t/unit-testing-with-gtest-gmock-on-env-desktop-on-arduino-platform/14354/7" target="_blank" rel="noopener noreferrer" class="external-link"&gt;diesen Thread&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; im Forum von PlatformIO, wenn es Fragen zu den folgenden Schritten gibt.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Unix-Benutzer: &lt;a href="https://blog.schallbert.de/projects/platformio_gtestgmock/#mingw-konfigurieren"&gt;bis hier&lt;/a&gt; die Schritte überspringen, denn wahrscheinlich werden nur die &lt;code&gt;libpthreadgc&lt;/code&gt;-Bibliotheken zusätzlich benötigt. Prüfen Sie das Vorhandensein aller Bibliotheken (aber lassen Sie das Präfix &lt;code&gt;mingw32-&lt;/code&gt; weg) und installieren ggf. fehlende nach. Stellen Sie sicher, dass die &lt;code&gt;PATH&lt;/code&gt;-Variable korrekt gesetzt ist.&lt;/li&gt;&#10;&lt;li&gt;Windows-Benutzer:&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Laden Sie MinGW herunter, z.B. von &lt;a href="https://sourceforge.net/projects/mingw" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Sourceforge&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Installieren Sie durch Ausführen von &lt;code&gt;mingw-get-setup.exe&lt;/code&gt;. Wenn Sie grafische Benutzeroberflächen mögen, lassen Sie das entsprechende Kästchen angekreuzt. Ich empfehle &lt;em&gt;nicht&lt;/em&gt;, das Installationsverzeichnis zu ändern. Wenn Sie das doch tun, müssen Sie sich sich den neuen Pfad merken. &lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/platformio_gtestgmock/mingw_install.jpg" alt="MinGW install" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="mingw-konfigurieren"&gt;MinGW konfigurieren&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Damit GoogleTest läuft, müssen zusätzliche Pakete installiert werden:&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;mingw32-gcc-g++&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;mingw32-libmingwex&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;mingw32-libmingwex-dll&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;mingw32-libmingwex-dev&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;mingw32-libpthreadgc-dll&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;mingw32-libpthreadgc-dev&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;Um diese zu installieren,&#10;&lt;ul&gt;&#10;&lt;li&gt;Wenn Sie die GUI verwenden: Suchen Sie die Bibliotheken unter &amp;ldquo;MinGW Standard Libraries&amp;rdquo; und fügen Sie sie nacheinander hinzu/aktivieren/installieren Sie sie.&lt;/li&gt;&#10;&lt;li&gt;Benutzer des Kommandozeilen-Interpreters: Geben Sie &lt;code&gt;mingw-get install&lt;/code&gt; ein und fügen Sie dann den Paketnamen hinzu.&#10;Wenn Sie eine Fehlermeldung erhalten, die besagt, dass einige der Pakete bereits existieren, umso besser.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;Jetzt müssen Sie den Ordner &amp;ldquo;bin&amp;rdquo; von MinGW zu Ihren Systempfadvariablen hinzufügen, damit PlatformIO sie später finden kann. Wenn Sie ihn nicht von Hand modifiziert haben, sollte er &lt;code&gt;C:\MinGW\bin&lt;/code&gt; lauten.&#10;&lt;ul&gt;&#10;&lt;li&gt;GUI-Fans: folgen Sie &lt;a href="https://www.architectryan.com/2018/03/17/add-to-the-path-on-windows-10" target="_blank" rel="noopener noreferrer" class="external-link"&gt;dieser Anleitung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;CLI: Geben Sie &lt;code&gt;set PATH=%PATH%;C:\MinGW\bin&lt;/code&gt; ein und überprüfen Sie, ob es mit &lt;code&gt;echo %PATH:;=&amp;amp;echo.%&lt;/code&gt; funktioniert.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Glückwunsch! Sie haben MinGW installiert, die Umgebungsvariablen so gesetzt, dass es von Drittanwendungen leichter gefunden wird, und es für die Zusammenarbeit mit GoogleTest konfiguriert.&lt;/p&gt;&#10;&lt;h3 id="platformio-konfigurieren"&gt;PlatformIO konfigurieren&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Nachdem die Variable &lt;code&gt;PATH&lt;/code&gt; gesetzt ist, muss VScode neu gestartet werden.&lt;/li&gt;&#10;&lt;li&gt;Damit PlatformIO korrekt mit dem gerade installierten Compiler verbunden wird, muss die &lt;code&gt;native&lt;/code&gt; Umgebung verwendet werden. Dort befindet sich der Compiler mit allen benötigten Umgebungsvariablen. Öffnen Sie ein PlatformIO-Terminal in einem Ihrer Projekte und geben Sie &lt;code&gt;pio platform install native&lt;/code&gt; ein. &lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/platformio_gtestgmock/install_native.jpg" alt="Image: how to install PIO native" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;Zwei Möglichkeiten:&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;A) Befolgen dieser Anleitung unter Verwendung meines Beispielprojektes wie &lt;a href="https://blog.schallbert.de/projects/platformio_gtestgmock/#writing-tests"&gt;unten&lt;/a&gt; beschrieben. Dann kann der nächste Abschnitt getrost übersprungen werden, da dieses Projekt vorkonfiguriert ist. Sie können nach Belieben damit herumspielen denn Sie verfügen von Anfang an über grundlegende Hardware-Abstraktionsschnittstellen.&lt;/li&gt;&#10;&lt;li&gt;B) Nehmen Sie Ihr eigenes Projekt und ändern Sie dessen &lt;code&gt;Platformio.ini&lt;/code&gt; so, dass GoogleTest ausgeführt werden kann.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;ol start="4"&gt;&#10;&lt;li&gt;Dies sollten Sie zu Ihrer &lt;code&gt;Platformio.ini&lt;/code&gt; hinzufügen:&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#ae81ff"&gt;env:desktop]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;platform = native&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;build_flags = &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#9;-&lt;span style="color:#ae81ff"&gt;std=gnu++11&lt;/span&gt; &lt;span style="color:#75715e"&gt;# use installed GNU C++11 compiler.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#9;-&lt;span style="color:#ae81ff"&gt;pthread&lt;/span&gt; &lt;span style="color:#75715e"&gt;# found in gtest documentation&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;lib_deps = &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#9;&lt;span style="color:#ae81ff"&gt;googletest&lt;/span&gt; &lt;span style="color:#75715e"&gt;# Will automatically load latest googletest lib&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;lib_ignore = &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;src&lt;/span&gt; &lt;span style="color:#75715e"&gt;# most main.cpp&amp;#39;s directly access the hardware. &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Can be removed if your project is different.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;#&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# PLUS: Any files that contain bare-metal hardware commands &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# (e.g. digitalWrite), g++ won&amp;#39;t have the headers or specialized compiler knowledge!&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;lib_compat_mode = off&lt;/span&gt; &lt;span style="color:#75715e"&gt;# Must-have for external stuff like gtest!&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Erklärung: Für die Umgebung &lt;code&gt;Desktop&lt;/code&gt; (= Computer ohne Microcontroller) wird der &lt;code&gt;native&lt;/code&gt; Compiler verwendet, d.h. GCC/G++, der oben in der Anleitung installiert und für den die &lt;code&gt;PATH&lt;/code&gt;-Variable festgelegt wurde. GoogleTest verwendet den C++11-Standard und Threading, der zu den Build-Flags hinzugefügt werden muss.&#10;Der Build von der &lt;code&gt;googletest&lt;/code&gt; Bibliothek ab. Es werden alle Dateien/Ordner ignoriert, die Quell- oder Headerdateien mit hardwarebezogenen Befehlen oder Headern enthalten. Der Bibliothekskompatibilitätsmodus muss ausgeschaltet sein, damit der Library Dependency Finder &lt;code&gt;googletest&lt;/code&gt; einbeziehen kann.&lt;/p&gt;&#10;&lt;ol start="5"&gt;&#10;&lt;li&gt;(Optional) Falls Sie lieber GUI-Schaltflächen verwenden als eine Zeile in die Konsole zu tippen um die Tests zu starten, erstellen Sie eine benutzerdefinierte &lt;a href="https://docs.platformio.org/en/latest/projectconf/build_configurations.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Konfiguration&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="tests-ausführen"&gt;Tests ausführen&lt;/h3&gt;&#10;&lt;p&gt;Für einen schnelleren Einstieg kann &lt;a href="https://github.com/Schallbert/PlatformIO_gTestgMock" target="_blank" rel="noopener noreferrer" class="external-link"&gt;mein Beispiel-Repository&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; geklont werden (den &lt;em&gt;main&lt;/em&gt; branch verwenden). Es enthält alle notwendigen Elemente, um zu prüfen, ob &lt;code&gt;gTest&lt;/code&gt; und &lt;code&gt;gMock&lt;/code&gt; korrekt funktionieren. So kann man direkt nachvollziehen, ob die Installation erfolgreich war.&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Entpacken Sie die Zip-Datei bzw. verwenden Sie SSH, um das Repository zu klonen.&lt;/li&gt;&#10;&lt;li&gt;Öffnen Sie die Startseite der PlatformIO-Erweiterung, klicken Sie auf &amp;ldquo;Open Project&amp;rdquo;, und wählen Sie den Ordner aus.&lt;/li&gt;&#10;&lt;li&gt;Um die Unittests auszuführen, öffnen Sie ein PlatformIO-Terminal und geben Sie &lt;code&gt;pio test -vvv -e desktop -f test_desktop&lt;/code&gt; ein. (-vvv = wortreich -e = Umgebung, -f = Filter). Die Parameter sind erforderlich, weil die Tests nur auf der &amp;ldquo;Desktop&amp;rdquo;-Umgebung lauffähig sind, nicht aber auf dem Microcontroller.&lt;/li&gt;&#10;&lt;li&gt;Nach einiger Zeit sollte die unten dargestellte Meldung angezeigt werden.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/platformio_gtestgmock/build_alltest_success.jpg" alt="successful build and test" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Super! Unit-Tests innerhalb von PlatformIO wurden gebaut und ausgeführt!&lt;/p&gt;&#10;&lt;h2 id="tipps-und-tricks"&gt;Tipps und Tricks&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;Zu beachten ist&lt;/strong&gt;, dass der Ordner &lt;code&gt;PlatformIO_gTestgMock/.pio/build/desktop&lt;/code&gt; nun eine ausführbare Datei namens &lt;code&gt;program.exe&lt;/code&gt; enthält. Wird sie über die Kommandozeile ausgeführt, sollte das selbe Ergebnis wie in der obigen Abbildung herauskommen. Dies kann nützlich sein, wenn Tests nicht nur fehlschlagen, sondern das ganze Programm bei der Ausführung abstürzt. Zum Debuggen der Tests ist das auch hilfreich, da die CLI von PlatformIO dazu neigt, sich aufzuhängen. Man hat hier auch leichten Zugang zum Stracktrace - das sprengt den Rahmen dieser Übung aber bei Weitem.&lt;/p&gt;&#10;&lt;p&gt;Ab diesem Punkt&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;können Sie weiterlesen, um den Inhalt des Repositorys und die Funktionsweise von GoogleTest besser zu verstehen&lt;/li&gt;&#10;&lt;li&gt;Sie könnten das Schreiben von Tests üben, indem Sie weitere Fälle hinzufügen, z. B. um zu überprüfen, ob der Zustand &amp;ldquo;LOW&amp;rdquo; korrekt durchlaufen wird.&lt;/li&gt;&#10;&lt;li&gt;Sie könnten auch die Interfaces dieser Grundbausteine verbessern, z. B. durch Hinzufügen der Funktion &lt;code&gt;analogRead()&lt;/code&gt; zur Hardware-Abstraktionsschicht&lt;/li&gt;&#10;&lt;li&gt;Sie könnten Ihr eigenes Projekt darauf aufbauen, die Hardware-Abhängigkeiten sauber ausschalten und es so einfach auf verschiedene Hardware-Geräte und Hersteller portieren&lt;/li&gt;&#10;&lt;li&gt;Oder Sie beschäftigen sich weiter mit diesen anderen genialen Dingen, die Sie in die Tat umsetzen wollten, bevor Sie von diesem Tutorial abgelenkt wurden.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="repository-inhalte-erklärt"&gt;Repository-Inhalte erklärt&lt;/h2&gt;&#10;&lt;p&gt;Die folgenden Abschnitte behandeln die meisten Dateien im Repository und erklären, was sie im Detail tun. Die implementierte Geschäftslogik ist minimal und sollte als Beispielcode betrachtet werden. Ein reales Projekt, das alle auf dieser Seite dargestellten Muster verwendet, ist auf meiner &lt;a href="https://blog.schallbert.de/projects/tonuino/"&gt;Tonuino&lt;/a&gt;-Seite zu finden.&lt;/p&gt;&#10;&lt;h3 id="schnittstelle-der-hardware-abstraktionsschicht"&gt;Schnittstelle der Hardware-Abstraktionsschicht&lt;/h3&gt;&#10;&lt;p&gt;Schauen wir uns das Interface und seine Struktur genauer an.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// hal_if.h&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;namespace&lt;/span&gt; hardwareAbstraction {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Hardware Abstraction Layer Interface class&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Hal_IF&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;virtual&lt;/span&gt; &lt;span style="color:#f92672"&gt;~&lt;/span&gt;Hal_IF() &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;default&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;virtual&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;digitalRead&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; pinId) &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;virtual&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;digitalWrite&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; pinId, &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; value) &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;} &lt;span style="color:#75715e"&gt;// hardwareAbstraction&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Die Klasse &lt;code&gt;Hal_IF&lt;/code&gt; hat zwei Methoden, die beide &lt;code&gt;virtuell&lt;/code&gt; sind. Das bedeutet, dass eine Client-Klasse, die diese Schnittstelle verwendet, die Methode aufrufen kann und somit &amp;ldquo;automatisch&amp;rdquo; an die konkrete Implementierung der Methode weitergeleitet wird. Das &lt;code&gt;= 0;&lt;/code&gt; macht diese Methoden &lt;em&gt;rein virtuell&lt;/em&gt;, so dass das Überschreiben dieser Methoden durch eine abgeleitete Klasse nicht optional sondern vorgeschrieben ist. Das ist genau das, was wir hier wollen, denn wenn diese Methode nicht überschrieben würde, hätte sie kein Verhalten.&#10;Zu beachten ist, dass der Destruktor einer Schnittstellenklasse &lt;a href="https://stackoverflow.com/questions/3628529/should-c-interfaces-have-a-virtual-destructor" target="_blank" rel="noopener noreferrer" class="external-link"&gt;fast immer virtuell sein sollte&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="loopinout"&gt;LoopInOut&lt;/h3&gt;&#10;&lt;p&gt;Die zu testende Logik. Sie greift nicht direkt auf die Ein-/Ausgabe-Hardware des µC zu, sondern auf die Variable &lt;code&gt;m_hal&lt;/code&gt;, die eine Implementierung der Schnittstelle &lt;code&gt;Hal_IF&lt;/code&gt; ist. Sie wird bei der Objekterzeugung per Referenz (gekennzeichnet durch das &lt;code&gt;&amp;amp;&lt;/code&gt; bei der Typdeklaration) an die Klasse übergeben&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// loopInToOut.h&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;LoopInToOut&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; LoopInToOut(hardwareAbstraction&lt;span style="color:#f92672"&gt;::&lt;/span&gt;Hal_IF&lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; hal)&lt;span style="color:#f92672"&gt;:&lt;/span&gt; m_hal(hal){};&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;~&lt;/span&gt;LoopInToOut() &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;default&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;loopThrough&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; inPin, &lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; outPin);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;private&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hardwareAbstraction&lt;span style="color:#f92672"&gt;::&lt;/span&gt;Hal_IF&lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; m_hal;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dies ermöglicht die eigentliche Magie von gMock: Da der Client-Code mit einer Schnittstelle arbeitet, kann ich die &amp;ldquo;echte&amp;rdquo; Implementierung (des Hardware-Zugriffs) zur µC-Laufzeit übergeben, aber für den Test kann ich eine &amp;ldquo;Modell&amp;rdquo;-Implementierung übergeben, über die ich zum Zeitpunkt der Ausführung des Tests die Kontrolle habe.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// loopInToOut.cpp&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; LoopInToOut&lt;span style="color:#f92672"&gt;::&lt;/span&gt;loopThrough(&lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; inPin, &lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; outPin)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; input &lt;span style="color:#f92672"&gt;=&lt;/span&gt; m_hal.digitalRead(inPin);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; m_hal.digitalWrite(outPin, input);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; input;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;loopThrough()&lt;/code&gt; ist wiederum sehr einfach. Sie liest den Zustand des Eingangspins und ordnet ihn dem Ausgangspin zu. Sie gibt den Zustand des Eingangspins an den Aufrufer zurück.&lt;/p&gt;&#10;&lt;h3 id="modellierte-hardwareabstraktion-mock-hal"&gt;Modellierte Hardwareabstraktion (mock hal)&lt;/h3&gt;&#10;&lt;p&gt;GoogleTest verwendet Makros, um Verhalten zu definieren. Die Mock-Klasse leitet sich ebenfalls von der Schnittstelle ab und stellt lediglich die Deklarationen der Schnittstellenmethoden bereit, die sie überschreibt. Es ist noch kein Verhalten festgelegt.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// hal_mock.h&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Hal_mock&lt;/span&gt; &lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; hardwareAbstraction&lt;span style="color:#f92672"&gt;::&lt;/span&gt;Hal_IF&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; MOCK_METHOD(&lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt;, digitalRead, (&lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; pinId), (&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt;));&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; MOCK_METHOD(&lt;span style="color:#66d9ef"&gt;void&lt;/span&gt;, digitalWrite, (&lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; pinId, &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; value), (&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt;));&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="die-testvorrichtung-test-fixture"&gt;Die Testvorrichtung (test fixture)&lt;/h4&gt;&#10;&lt;p&gt;Die Testvorrichtung enthält gemeinsame Daten und Verhaltensweisen für alle Testfälle innerhalb einer Testsuite. Name der Testsuite = Klassenname. Genau dieser Name muss in den Tests, die zu dieser Suite gehören sollen, wiederverwendet werden.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// unittest_loopInToOut.cpp&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;loopInToOut_Test&lt;/span&gt; &lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#f92672"&gt;::&lt;/span&gt;testing&lt;span style="color:#f92672"&gt;::&lt;/span&gt;Test&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;protected&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;virtual&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; SetUp()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; m_loopInToOut &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; LoopInToOut(hal_mock);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;virtual&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;TearDown&lt;/span&gt;()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;delete&lt;/span&gt; m_loopInToOut;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; NiceMock&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Hal_mock&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; hal_mock{};&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; LoopInToOut&lt;span style="color:#f92672"&gt;*&lt;/span&gt; m_loopInToOut{&lt;span style="color:#66d9ef"&gt;nullptr&lt;/span&gt;};&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Jeder Test verfügt also über einige vorbereitete Daten, die er verwenden kann: Die zu testende Klasse wird als Zeiger mit einer modellierten (Mock) Implementierung ihrer Abhängigkeit eingerichtet.&lt;/p&gt;&#10;&lt;h3 id="writing-tests"&gt;Der eigentliche Test&lt;/h3&gt;&#10;&lt;p&gt;Der Test wird durch das Makro &lt;code&gt;TEST_F&lt;/code&gt; definiert, was soviel bedeutet wie &amp;ldquo;Test mit Fixture&amp;rdquo;, das eine Testsuite und Testnamen erwartet.&#10;Das Makro &lt;code&gt;ON_CALL&lt;/code&gt; ist ein Befehl an gMock, der der modellierten Klasse vorgibt, wie sie sich zu verhalten hat, wenn &lt;code&gt;digitalRead(_)&lt;/code&gt; mit einer beliebigen Eingabe (Unterstrich = beliebig) aufgerufen wird: die Methode soll standardmäßig &lt;code&gt;true&lt;/code&gt; zurückgeben.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// unittest_loopInToOut.cpp&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;TEST_F(loopInToOut_Test, loop_inputHigh_writesCorrectOutputHigh)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ON_CALL(hal_mock, digitalRead(_)).WillByDefault(Return(true));&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; EXPECT_CALL(hal_mock, digitalWrite(&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, true));&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; m_loopInToOut&lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt;loopThrough(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dann wird eine &amp;ldquo;Erwartung&amp;rdquo; an die nachgebildete Klasse formuliert, die besagt, dass &lt;code&gt;digitalWrite&lt;/code&gt; mit Eingabewerten von &lt;code&gt;(2, true)&lt;/code&gt; aufgerufen wird, die ebenfalls auf Korrektheit geprüft werden.&lt;/p&gt;&#10;&lt;p&gt;Die letzte Anweisung ruft die echte Implementierung auf, so dass die Erwartungen überprüft werden können.&lt;/p&gt;&#10;&lt;h3 id="implementierung-des-hardwareverhaltens"&gt;Implementierung des Hardwareverhaltens&lt;/h3&gt;&#10;&lt;p&gt;Da &lt;code&gt;main.cpp&lt;/code&gt; auf die Hardware zugreift, muss es dafür eine Art Schnittstellenimplementierung geben.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// hal.h&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Hal&lt;/span&gt; &lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; Hal_IF&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Hal() &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;default&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;~&lt;/span&gt;Hal() &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;default&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;digitalRead&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; pinId) &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;digitalWrite&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; pinId, &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; value) &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hier leitet &lt;code&gt;Hal&lt;/code&gt; sich von &lt;code&gt;Hal_IF&lt;/code&gt; ab und überschreibt alle seine Methoden.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c++" data-lang="c++"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// hal.cpp&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#include&lt;/span&gt; &lt;span style="color:#75715e"&gt;&amp;#34;Arduino.h&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#include&lt;/span&gt; &lt;span style="color:#75715e"&gt;&amp;#34;hal.h&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;namespace&lt;/span&gt; hardwareAbstraction{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; Hal&lt;span style="color:#f92672"&gt;::&lt;/span&gt;digitalRead(&lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; pinId) &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;digitalRead&lt;/span&gt;(pinId);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; Hal&lt;span style="color:#f92672"&gt;::&lt;/span&gt;digitalWrite(&lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; pinId, &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; value) &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; digitalWrite(pinId, value);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Man erkennt, dass nur diese Quelldatei tatsächlich die &lt;code&gt;Arduino.h&lt;/code&gt; für den Zugriff auf die Hardware einbindet. Die Abhängigkeit von den Hardware-Funktionen ist also sehr begrenzt und in Schnittstellen-Implementierungen gekapselt, die durch die Programmstruktur ausgewählt werden können. Während die &lt;code&gt;main.cpp&lt;/code&gt; für das Zielgerät also ein Objekt der obigen Implementierung erzeugt, wird der Test stattdessen ein Mock übergeben.&lt;/p&gt;&#10;&lt;h2 id="gtest-schlüsselkonzepte"&gt;Gtest Schlüsselkonzepte&lt;/h2&gt;&#10;&lt;p&gt;Googles offizielle &lt;a href="http://google.github.io/googletest/primer.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Webseite über Googletest&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; kann sehr helfen, nicht nur für den Einstieg&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;. Das gmock Cookbook ist ein hervorragender Leitfaden, den ich im Laufe der Monate benutzt habe, als meine Tests immer komplexer und schwieriger zu schreiben wurden, und das gmock Cheat Sheet hilft, wenn man den Überblick verloren hat, wie ein Test für dieses oder jenes spezielle Problem zu schreiben ist.&lt;/p&gt;&#10;&lt;h3 id="dependency-injection"&gt;Dependency Injection&lt;/h3&gt;&#10;&lt;p&gt;Dependency Injection ist die grundlegende Mechanik, verstanden werden muss, damit man gMock erfolgreich anwenden kann. Ich versuche es mal mit einem Bild zu verdeutlichen.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/platformio_gtestgmock/dependencyinjection.jpg" alt="Dependency Injection using interfaces" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;&lt;em&gt;Kurzgesagt ist die Schnittstelle lediglich ein Platzhalter für die Implementierung der Abhängigkeit der Client-Klassen.&lt;/em&gt;&lt;/p&gt;&#10;&lt;p&gt;Zur Laufzeit, z.B. wenn der Host das Client-Objekt erzeugt, wird er die Implementierung der Abhängigkeit an den Client übergeben (d.h. injizieren), der dann jede seiner Methoden aufrufen kann, da ihre Signaturen zur Kompilierzeit aufgrund der vom Client verwendeten Schnittstelle bekannt sind.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Das Mock ist nun lediglich eine Implementierung der Schnittstelle, die für die Tests spezialisiert ist.&lt;/li&gt;&#10;&lt;li&gt;Die eigentliche Implementierung wird in allen anderen Kontexten verwendet, sie zeigt das &amp;ldquo;echte&amp;rdquo; Verhalten dieser Klasse.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="hardware-abstraktionsschicht"&gt;Hardware-Abstraktionsschicht&lt;/h3&gt;&#10;&lt;p&gt;Dies ist vielleicht einer der kniffligen Punkte bei der Arbeit mit eingebetteter Software.&#10;Irgendwann geht es um die Interaktion mit der Außenwelt, sei es mit GPIO-Pins (General Purpose Input Output), den Zugriff auf eines der unzähligen Hardware-Bussysteme wie I2C, UART, SPI, CAN zur Steuerung externer Hardware oder einfach die Nutzung von Systeminterna wie dem EEPROM - da fangen die Probleme an.&lt;/p&gt;&#10;&lt;p&gt;Gtest läuft auf dem GCC/G++ Compiler, der einfach nichts von dem weiß, was der spezialisierte Compiler des jeweiligen Chips weiß. Er würde nicht einmal die Header-Dateien verstehen. Nun gibt es zwei Möglichkeiten, dieses Problem zu umgehen:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&amp;ldquo;Fake Header&amp;rdquo; für jede µC-spezifische Implementierung schreiben, die von GCC/G++ verwendet werden können&lt;/li&gt;&#10;&lt;li&gt;Eine Schnittstelle für jede µC-spezifische Implementierung unter Verwendung einer (Hardware-)Abstraktionsschicht programmieren, so dass Ihre Produktionsumgebung die Chip-Hardware wie gewohnt verwendet, die Testumgebung aber stattdessen Mocks benutzt.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Der zweite Ansatz hat eine Reihe von Vorteilen:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Er ist sauber. Er zwingt einen dazu, keine Abstraktionsschichten zu zusammenzufassen die getrennt gehören.&lt;/li&gt;&#10;&lt;li&gt;Er ist einfach. Man kann die Geschäftslogik testen, ohne den µC zu programmieren! Es müssen nur die Eingänge gemockt und die Ausgänge abgefangen werden.&lt;/li&gt;&#10;&lt;li&gt;Er ist portabel. Wenn man die Implementierung Abstraktionsschicht austauscht, kann man sie auf einer anderen Controller-Familie oder sogar auf Chips eines anderen Herstellers auszuführen.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="nachteile"&gt;Nachteile&lt;/h3&gt;&#10;&lt;p&gt;Hmm, wo Licht ist, muss es doch irgendwo auch Schatten geben. Meiner Meinung nach sind die Nachteile, wenn man alles ohne angeschlossene Hardware testen will:&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/platformio_gtestgmock/memoryfull.jpg" alt="nicht viel Speicher übrig..." /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Speicher. Sowohl ROM als auch RAM werden schneller voll, wenn eine große Anzahl von Klassen und vtables existieren.&lt;/li&gt;&#10;&lt;li&gt;Besitz. Die Injektion von Abhängigkeiten trennt den Besitz von der Nutzung eines Objekts, und man muss sich entscheiden, wie beides organisiert werden soll.&lt;/li&gt;&#10;&lt;li&gt;Bibliotheken von Drittanbietern können nicht einfach verwendet werden sondern benötigen Anpassung. Ein Interface-Header, eine &lt;a href="https://refactoring.guru/design-patterns/adapter/cpp/example" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Adapter-Implementierung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und ein Mock für jede Bibliothek, die &amp;ldquo;chip-internals&amp;rdquo; verwendet, sind zu erstellen.&lt;/li&gt;&#10;&lt;li&gt;Komplexität. Da Objekte über eine Schnittstelle injiziert werden, muss jetzt Call-by-Reference oder Übergabe per Zeiger durchgeführt werden 😨. Ersteres ist zusätzlich eingeschränkt, da es nur für Konstruktorinjektion funktioniert, nicht für andere Arten (Methodeninjektion, Setterinjektion usw.).&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="einige-weitere-tipps"&gt;Einige weitere Tipps&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Alles &amp;ldquo;hinter&amp;rdquo; der Abstraktionsschicht sollte so einfach wie möglich gehalten werden: Diese Dinge können nur auf dem Zielgerät getestet und ausgeführt werden. Beispiel: Jede Methode führt nur einen Befehl aus (wie in &lt;a href="https://github.com/Schallbert/PlatformIO_gTestgMock/blob/main/lib/hal/hal.cpp" target="_blank" rel="noopener noreferrer" class="external-link"&gt;meinem Beispielcode&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;).&lt;/li&gt;&#10;&lt;li&gt;Verwendung einer Loader-Klasse oder eines &lt;a href="https://www.fluentcpp.com/2019/06/07/write-your-own-dependency-injection-container" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Dependency Injection Container (Nicolas Croad)&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; erleichtert die Arbeit, denn er enthält alle zu injizierenden Objekte und kann sie auf Anfrage übergeben.&lt;/li&gt;&#10;&lt;li&gt;Die &lt;a href="https://blog.schallbert.de/projects/platformio_gtestgmock/#nachteile"&gt;oben genannten Nachteile&lt;/a&gt; sollten im Blick behalten werden. Bei kleineren Projekten lohnt sich dieser Aufwand wahrscheinlich nicht.&lt;/li&gt;&#10;&lt;li&gt;Manchmal macht es sogar Sinn, mehrere Ebenen Schnittstellen zu haben. Dieses Muster kann nicht nur für die direkte Interaktion mit der Hardware zu verwendet werden, sondern auch zwischen Softwaremodulen, die dann in einer höheren Granularität getestet werden können. Dies kann sogar schneller sein als ein großen Haufen Klassen zu haben, die nur über Mocks in der Hardware-Abstraktionsschicht kontrolliert werden können.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="schwieriges-thema"&gt;Schwieriges Thema!&lt;/h3&gt;&#10;&lt;p&gt;Sie haben sich jetzt eine Pause verdient. Das waren viele Informationen, die Sie verarbeiten mussten. Nehmen Sie sich Zeit. Kontaktieren Sie mich gern auf der &lt;a href="https://github.com/Schallbert/PlatformIO_gTestgMock/discussions/1" target="_blank" rel="noopener noreferrer" class="external-link"&gt;discussions&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Seite zu diesem Thema, wenn Sie das Gefühl haben, dass etwas fehlt, nicht korrekt dargestellt ist, verbessert werden sollte oder einfach genau das ist, was Sie jetzt brauchten.&lt;/p&gt;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;Die Installation unter Linux ist einfacher, da &lt;code&gt;gcc&lt;/code&gt; und &lt;code&gt;g++&lt;/code&gt; bereits vorinstalliert sind. Selbst wenn sie noch nicht die richtigen Pakete zur Unterstützung von GoogleTest enthalten, sind ihr Fehlermeldungen viel aussagekräftiger als die unter Windows, z.B. wenn Bibliotheken fehlen. 🙃&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:2"&gt;&#10;&lt;p&gt;Dies wird auch &lt;a href="https://github.com/ninject/Ninject/wiki/Injection-Patterns" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Constructor Injection&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; genannt, eine Unterart der &lt;a href="https://github.com/ninject/ninject/wiki/Dependency-Injection-By-Hand" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Dependency Injection&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, die ein Mittel zur Erreichung der &lt;a href="https://en.wikipedia.org/wiki/Inversion_of_control" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Inversion Of Control&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ist. Ich habe einige Links zu weiteren Informationen vorbereitet. Dies ist ein umfangreiches Thema, über das ganze Bücher geschrieben worden sind.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:3"&gt;&#10;&lt;p&gt;Übrigens: die googletest-Seite ist auch statisch wie meine und verwendet ebenfalls Jekyll 👋 &amp;hellip;gefällt sie Ihnen? Ich würde gern Ihre Meinung dazu &lt;a href="https://github.com/Schallbert/schallbert.github.io/discussions" target="_blank" rel="noopener noreferrer" class="external-link"&gt;auf meiner Diskussionsseite&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; erfahren.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>QR-Codengrave</title><link>https://blog.schallbert.de/projects/qr-codengrave/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/qr-codengrave/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/qr-codengrave/app-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: qr-code created with my CNC portal milling machine"&#10; title="QR-Codengrave" /&gt;&#10;&lt;h2 id="projekt-steckbrief"&gt;Projekt-Steckbrief&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Schwierigkeitsgrad: Schwer 4/5&lt;/li&gt;&#10;&lt;li&gt;Kosten: 0€&lt;/li&gt;&#10;&lt;li&gt;Zeitaufwand: ~50h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="die-idee"&gt;Die Idee&lt;/h2&gt;&#10;&lt;p&gt;Ein Freund bat mich, einen Bierdeckel für ihn anzufertigen. Ein eigenes Logo, ein paar Buchstaben. Nichts allzu Schweres. Auf der Rückseite wollte er einen &lt;a href="https://de.wikipedia.org/wiki/QR_code" target="_blank" rel="noopener noreferrer" class="external-link"&gt;QR-Code&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; eingravieren lassen, der einen Link zu seiner Website enthält.&lt;/p&gt;&#10;&lt;p&gt;Ich habe also einen QR-Code mit einem &lt;a href="https://www.nayuki.io/page/qr-code-generator-library" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Standard-Webtool&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; erstellt und als &lt;code&gt;.svg&lt;/code&gt; exportiert. Leider müsste ich in meinem CAM jedes einzelne Pixel anklicken und eine Carve-Action für dieses Pixel erstellen, was einige Minuten meiner Zeit für Auswahl-Klick-und-Parameter-Wähl-Prozeduren in Anspruch nehmen würde. Außerdem müsste ich einen Gravurstichel mit tiefenvariabler Gravur verwenden, um ein quadratisches Pixel zu erhalten, was viel mehr Bearbeitungszeit erfordert als eine runde Darstellung.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/qr-codengrave/svg_path_issues.jpg" alt="Bild: QR-Code SVG in meinem CAM" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;An diesem Punkt beschloss ich, viele, viele weitere Stunden meiner Zeit auf dieses Problem zu werfen und eine allgemeine Lösung für QR-Code-Gravuren zu entwickeln - eine Software namens &lt;strong&gt;QR-codengrave&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;h2 id="erste-schritte"&gt;Erste Schritte&lt;/h2&gt;&#10;&lt;h2 id="spezifikation"&gt;Spezifikation&lt;/h2&gt;&#10;&lt;p&gt;Das, was ich mit dem Tool erreichen wollte:&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;Muss es können:&lt;/em&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Texteingabe verarbeiten und in einen QR-Code konvertieren. Der Benutzer muss keine zusätzliche Konfiguration vornehmen.&lt;/li&gt;&#10;&lt;li&gt;Bietet die Option zur Verwaltung verschiedener CNC-Werkzeuge.&lt;/li&gt;&#10;&lt;li&gt;Ermöglicht Eingabe von Gravurparametern: Gravurtiefe, Eilgangbewegung im Job, Überflughöhe&lt;/li&gt;&#10;&lt;li&gt;Ermöglicht Eingaben für benutzerdefinierte XY0-Offsets für Werkstücke&lt;/li&gt;&#10;&lt;li&gt;Vektorisiert die Pixelausgabe des QR-Codes&lt;/li&gt;&#10;&lt;li&gt;Kann Pfade aus den Vektoren erstellen lassen, beginnt der Einfachheit halber mit einer Spirale nach innen&lt;/li&gt;&#10;&lt;li&gt;Übergabe der Pixellinien an ein Modul, das die Parameter des ausgewählten Werkzeugs verwendet, um Maschinenbahnen zu erstellen&lt;/li&gt;&#10;&lt;li&gt;Maschinenbahnen in G-Code konvertieren, der später von meiner Maschine interpretiert werden kann.&lt;/li&gt;&#10;&lt;li&gt;Gründliche Überprüfung der Algorithmen mit automatisierten Unit-Tests (bitte keine Abstürze in der realen Welt)&lt;/li&gt;&#10;&lt;li&gt;Bietet eine grafische Benutzeroberfläche, um die Parametereingabe so einfach wie möglich zu machen.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;&lt;em&gt;Optional&lt;/em&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Eine Continuous-Integration-Pipeline sowohl für meine IDE als auch für Github-Aktionen erstellen&lt;/li&gt;&#10;&lt;li&gt;Eine Continuous-Deploy-Pipeline für meine IDE (PyInstaller) und Github-Aktionen (Artefakte, Releases) erstellen&lt;/li&gt;&#10;&lt;li&gt;Integrationstests für die GUI hinzufügen&lt;/li&gt;&#10;&lt;li&gt;Optimieren des Algorithmus zur Pfaderzeugung, da die &amp;ldquo;nach innen gerichtete Spirale&amp;rdquo; eine hohe Fertigungsqualität hat und schön anzusehen ist, aber nicht sehr effizient mit der Zeit umgeht.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="unbekannte"&gt;Unbekannte&lt;/h3&gt;&#10;&lt;p&gt;Zu Beginn dieses Projekts hatte ich einige offene Fragen und Risiken, die die Nutzbarkeit des Programms hätten einschränken oder sogar das gesamte Projekt undurchführbar machen können. Sie haben sich alle als falsch herausgestellt (zum Glück), aber dennoch sind hier die Risiken, die meine ganze Arbeit unbrauchbar hätten machen können:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Kameras lesen keine runden Punkt-als-Pixel-QR-Codes.&lt;/li&gt;&#10;&lt;li&gt;Der Kontrast, den meine Graviermaterialien bieten, ist zu gering, so dass das Lesen der Codes schlecht oder gar nicht funktioniert.&lt;/li&gt;&#10;&lt;li&gt;Die Bearbeitungszeit könnte sehr hoch sein, was das Projekt unwirtschaftlich machen würde.&lt;/li&gt;&#10;&lt;li&gt;Beschränkungen der Programmierwerkzeuge oder meiner Programmierfähigkeiten könnten die Entwicklungszeit so verlängern, dass es sich nicht lohnt, weitere Anstrengungen zu unternehmen.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Daher beschloss ich, &lt;a href="https://blog.schallbert.de/projects/qr-codengrave/#unit-tests"&gt;Unit-Tests&lt;/a&gt; zusammen mit dem Code zu schreiben, um ein schnelles Feedback darüber zu erhalten, ob mein Code das leisten kann, was er soll. Außerdem habe ich die Funktionalität des Programms auf eine Gravurstrategie beschränkt und die GUI so gestaltet, dass sie nur eine Vorschau des QR-Codes anzeigt, nicht aber XY-Offsets oder anderen Schnickschnack.&lt;/p&gt;&#10;&lt;h3 id="programmiersprache"&gt;Programmiersprache&lt;/h3&gt;&#10;&lt;p&gt;Ich nahm einige Blätter Papier und notierte mir das Wesentliche.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/qr-codengrave/sketches.jpg" alt="Bild: Softwareplanung auf Papier" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Ein Modul soll sich um die Erstellung eines QR-Codes aus einer Texteingabe kümmern, ein anderes soll Vektoren und Pfade erzeugen, um diesen QR-Code zu gravieren, und eine GUI in der Mitte soll die Texteingabe, eine Bearbeitungsvorschau und Eingabemöglichkeiten für Fräswerkzeuge, Gravurparameter und Werkstückversätze enthalten.&lt;/p&gt;&#10;&lt;p&gt;Ich hatte ein paar Programmiersprachen zur Auswahl, die ich gut genug &amp;ldquo;spreche&amp;rdquo;:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;C++ mit QT-Framework&lt;/li&gt;&#10;&lt;li&gt;Java&lt;/li&gt;&#10;&lt;li&gt;Python mit dem Tkinter-Framework&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Ich entschied mich für die - meiner Meinung nach - einfachste Lösung, um so schnell wie möglich einen lauffähigen Prototyp zu haben: Python mit Tkinter. Ich kenne Tkinter von einem meiner &lt;a href="https://github.com/Schallbert/DerWinterNaht" target="_blank" rel="noopener noreferrer" class="external-link"&gt;früheren Projekte&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und obwohl es seine Einschränkungen hat, ist es immer noch leistungsstark genug und schnell zu verwenden.&lt;/p&gt;&#10;&lt;h3 id="konzeptphase"&gt;Konzeptphase&lt;/h3&gt;&#10;&lt;p&gt;Nach etwas Recherche fand ich eine ziemlich gute Open-Source-&lt;a href="https://www.nayuki.io/page/qr-code-generator-library" target="_blank" rel="noopener noreferrer" class="external-link"&gt;QR-Code-Bibliothek im Internet&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, die auch in Python verfügbar ist. Auf der Grundlage ihrer Ausgaben habe ich einen Vektorisierungsalgorithmus erstellt, der die QR-Code-Bibliothek abfragen kann, um die Werte der Pixel des QR-Codes in einer Scanzeile zurückzugeben:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;_qr_bitstream_from_line&lt;/span&gt;(self, line):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;Creates a bitstream from an input line of a QR-code data representation&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; :param line: a QR-code data representation (line of bits within the QR-code)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; :returns bitstream: returns an array of bits reflecting the QR code&amp;#39;s state at the respective&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; point of the line.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; bitstream &lt;span style="color:#f92672"&gt;=&lt;/span&gt; []&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_direction() &lt;span style="color:#f92672"&gt;==&lt;/span&gt; Direction&lt;span style="color:#f92672"&gt;.&lt;/span&gt;RIGHT:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; x &lt;span style="color:#f92672"&gt;in&lt;/span&gt; range(line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_p_start()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;x, line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_p_end()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;x &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; bitstream&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_qr&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_module(x, abs(line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_p_start()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;y)))&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_direction() &lt;span style="color:#f92672"&gt;==&lt;/span&gt; Direction&lt;span style="color:#f92672"&gt;.&lt;/span&gt;LEFT:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; x &lt;span style="color:#f92672"&gt;in&lt;/span&gt; range(line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_p_start()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;x, line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_p_end()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;x &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; bitstream&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_qr&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_module(x, abs(line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_p_start()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;y)))&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_direction() &lt;span style="color:#f92672"&gt;==&lt;/span&gt; Direction&lt;span style="color:#f92672"&gt;.&lt;/span&gt;UP:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; y &lt;span style="color:#f92672"&gt;in&lt;/span&gt; range(line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_p_start()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;y, line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_p_end()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;y &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; bitstream&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_qr&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_module(line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_p_start()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;x, abs(y)))&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_direction() &lt;span style="color:#f92672"&gt;==&lt;/span&gt; Direction&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DOWN:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; y &lt;span style="color:#f92672"&gt;in&lt;/span&gt; range(line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_p_start()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;y, line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_p_end()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;y &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; bitstream&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_qr&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_module(line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_p_start()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;x, abs(y)))&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; bitstream&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dann musste ich aus diesen Linien Vektoren erstellen, so dass aufeinanderfolgende Pixel derselben Farbe zu einer Linie zusammengefügt werden konnten, die viel schneller herzustellen ist als einzelne Punkte:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;_vectorize_bitstream&lt;/span&gt;(self, bitstream):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;Creates a QrLineData object from an input bitstream&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; :param bitstream: an array of bits = a line of the QR-code&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; :returns line_vector: A list of QrLineData objects&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; line_vector &lt;span style="color:#f92672"&gt;=&lt;/span&gt; []&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; data &lt;span style="color:#f92672"&gt;=&lt;/span&gt; QrLineData(bitstream[&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;], &lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; bit &lt;span style="color:#f92672"&gt;in&lt;/span&gt; range(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, len(bitstream)):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; bitstream[bit] &lt;span style="color:#f92672"&gt;==&lt;/span&gt; data&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_state():&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; data&lt;span style="color:#f92672"&gt;.&lt;/span&gt;add_length()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; line_vector&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append(data)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; data &lt;span style="color:#f92672"&gt;=&lt;/span&gt; QrLineData(bitstream[bit])&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;not&lt;/span&gt; data&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_state():&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; data&lt;span style="color:#f92672"&gt;.&lt;/span&gt;finalize()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; line_vector&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append(data)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; line_vector&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Diese beiden Methoden würden nun von einer übergeordneten Methode aufgerufen werden, die einen spiralförmigen Pfad durch den QR-Code kennt.&#10;Meine erste GUI hatte nur drei Elemente: Ein Eingabefeld für den Text, der in einen QR-Code umgewandelt werden sollte, eine Schaltfläche, um den Algorithmus auszuführen, und eine Zeichenfläche, auf der der Gravurpfad des Algorithmus ausgegeben werden sollte.&lt;/p&gt;&#10;&lt;p&gt;Ich das Modul &lt;a href="https://docs.python.org/3/library/turtle.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Turtle&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; verwendet, um den vektorisierten QR-Code-Pfad zu zeichnen, ein Tool, das für Schülerinnen und Schüler geschrieben wurde, die sich in Programmiersprachen wie Python einarbeiten. Es ist zwar langsam, aber einfach einzurichten, so dass mir das für den Anfang genügte. Ich habe für&amp;rsquo;s Erste hart kodiert, um Zeit zu sparen, und meine Schleifen sind eher C-esk als pythonisch, aber egal.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; vect &lt;span style="color:#f92672"&gt;in&lt;/span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_spiral_path[i]&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_z_vector():&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_stop_draw:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;break&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; length &lt;span style="color:#f92672"&gt;=&lt;/span&gt; vect&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_length()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; vect&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_state():&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;turtle&lt;span style="color:#f92672"&gt;.&lt;/span&gt;down()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;turtle&lt;span style="color:#f92672"&gt;.&lt;/span&gt;forward(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pen_size &lt;span style="color:#f92672"&gt;*&lt;/span&gt; length)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;turtle&lt;span style="color:#f92672"&gt;.&lt;/span&gt;up()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;turtle&lt;span style="color:#f92672"&gt;.&lt;/span&gt;forward(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pen_size &lt;span style="color:#f92672"&gt;*&lt;/span&gt; length)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;turtle&lt;span style="color:#f92672"&gt;.&lt;/span&gt;right(&lt;span style="color:#ae81ff"&gt;90&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dieses sehr einfache Konzept sah für mich vielversprechend aus, aber ich musste ein paar Stunden daran feilen, weil meine anfängliche Ausgabe wie folgt aussah:&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/qr-codengrave/drawing_issue_cut.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;QR-codengrave GUI test&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Nach einer Weile konnte ich jedoch zuverlässig einen (21x21 Pixel) QR-Code auf den Bildschirm zeichnen und die Kamera meines Smartphones verwenden, um den Link zur Webseite zu erhalten. 🥳 Yeah!&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/qr-codengrave/working_scan.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;QR-codengrave Vektorerzeugung funktioniert&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h3 id="unit-tests"&gt;Unit-Tests&lt;/h3&gt;&#10;&lt;p&gt;Die &amp;ldquo;Plattform&amp;rdquo;-Teile - Klassen &lt;code&gt;vectorize_qr&lt;/code&gt; und &lt;code&gt;machinify_vector&lt;/code&gt; - enthalten beide eine Menge überprüfbarer Berechnungen und Geschäftslogik. Deshalb sind meine Tests mit Pythons Standardmodul &lt;a href="https://docs.python.org/3/library/unittest.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;unittest&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; schnell und einfach zu schreiben. In diesem Beispiel überprüfe ich, ob die von mir erstellte Linienklasse eine Länge (n+1 Stil) korrekt berechnet:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;TestLine&lt;/span&gt;(unittest&lt;span style="color:#f92672"&gt;.&lt;/span&gt;TestCase):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;test_line_x_calculates_valid_length&lt;/span&gt;(self):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; line &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Line(Point(&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;), Point(&lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;))&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;assertEqual(&lt;span style="color:#ae81ff"&gt;6&lt;/span&gt;, line&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_abs_length())&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Aber es wurde schnell komplexer, als ich testen wollte, ob die Ausgabe der QR-Code-Generatorbibliothek in meinem Programm korrekt verarbeitet wird. Dazu musste ich die Bibliothek in meine zu testende Klasse einbinden, damit ich eine Mock-Implementierung einschleusen konnte, die dann im Test abgefragt wird.&lt;/p&gt;&#10;&lt;p&gt;Dieses Muster der Abhängigkeitsinjektion ist der Vorgehensweise in &lt;code&gt;C++&lt;/code&gt; sehr ähnlich, wo ich viel mehr Erfahrung habe (siehe ein Projekt &lt;a href="https://blog.schallbert.de/projects/platformio_gtestgmock/#dependency-injection"&gt;hier&lt;/a&gt; oder ein anderes &lt;a href="https://github.com/Schallbert/Tonuino_alternativeFirmware" target="_blank" rel="noopener noreferrer" class="external-link"&gt;hier&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, zum Beispiel), nur dass in Python viel weniger Boilerplate-Code nötig ist, um dorthin zu gelangen. Ein paar Suchvorgänge im Internet und schon war der folgende Testfall geschrieben:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;test_bitstream_qr_input_loop_xpos_correct&lt;/span&gt;(self):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mock_qr &lt;span style="color:#f92672"&gt;=&lt;/span&gt; QrCode&lt;span style="color:#f92672"&gt;.&lt;/span&gt;encode_text(&lt;span style="color:#e6db74"&gt;&amp;#34;schallbert.de&amp;#34;&lt;/span&gt;, QrCode&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Ecc&lt;span style="color:#f92672"&gt;.&lt;/span&gt;MEDIUM)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mock_qr&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_module &lt;span style="color:#f92672"&gt;=&lt;/span&gt; MagicMock()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cam &lt;span style="color:#f92672"&gt;=&lt;/span&gt; VectorizeQr(mock_qr, &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cam&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_qr_bitstream_from_line(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;test_line_x)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mock_qr&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_module&lt;span style="color:#f92672"&gt;.&lt;/span&gt;assert_called_with(&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ich injiziere die &amp;ldquo;echte&amp;rdquo; &lt;code&gt;QrCode&lt;/code&gt;-Bibliothek, aber mit einer Mock-Methode, die für mich von Interesse ist, &lt;code&gt;get_module()&lt;/code&gt;. In dem Test stelle ich sicher, dass die Methode mit den erwarteten Parametern aus meiner zu testenden Klasse aufgerufen wird.&lt;/p&gt;&#10;&lt;p&gt;Mit solchen Tests kann ich sichergehen, dass die Bausteine meines Algorithmus wie vorgesehen funktionieren und dass Regressionen schnell gefunden werden können.&lt;/p&gt;&#10;&lt;h2 id="der-lange-weg-zum-produkt-mit-minimalanforderungen"&gt;Der lange Weg zum Produkt mit Minimalanforderungen&lt;/h2&gt;&#10;&lt;p&gt;Beflügelt von diesem schnellen Erfolg, ging ich aufs Ganze. Vor der Implementierung des G-Code-Generators musste ich einen Werkzeugselektor entwerfen, da ich die Werkzeugeigenschaften zur Verfügung haben musste, bevor ich die Abmessungen und die Werkzeuggeschwindigkeit / den Vorschub für die QR-Code-Gravur einstellen konnte.&lt;/p&gt;&#10;&lt;p&gt;Nach einigen erfolglosen Versuchen im Hauptfenster entschied ich, dass das Werkzeugkonfigurationsfenster stattdessen ein Dialog sein sollte (den Tkinter &lt;a href="https://www.pythontutorial.net/tkinter/tkinter-toplevel/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Toplevel&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; nennt), so dass die Werkzeugparameter eingegeben und nach Abschluss an die Haupt-GUI zurückgeschickt werden können. Ich habe den Dialog modal gemacht mit&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_dialog&lt;span style="color:#f92672"&gt;.&lt;/span&gt;grab_set()&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So hat er immer den Fokus, damit nicht mehrere Instanzen dieses Fensters gleichzeitig vorhanden sein können.&#10;Ich habe die konfigurierten Werkzeuge in einer nummerierten Liste gespeichert, so dass die Anwendung, sobald sie gefüttert wurde, die Liste zur Verfügung stellt, damit der Benutzer das gewünschte Werkzeug für den Job aus einem Dropdown-Menü auswählen kann.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/qr-codengrave/tool_config.jpg" alt="Bild: QR-codengrave&amp;#39;s tool configuration dialog" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;In diesem Schritt musste ich zwei Dinge implementieren:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Überprüfungen für Wertetyp und -bereich&lt;/li&gt;&#10;&lt;li&gt;Ein Persistenzmodul, so dass die eingegebenen Tools einen Neustart der Anwendung überleben würden.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="überprüfungen-von-wertetyp-und--bereich"&gt;Überprüfungen von Wertetyp und -bereich&lt;/h3&gt;&#10;&lt;p&gt;Dies könnte mit einem Ereignis-Auslöser innerhalb des Eingabefeld-Widgets wie folgt erreicht werden:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; reg &lt;span style="color:#f92672"&gt;=&lt;/span&gt; config_tool_frame&lt;span style="color:#f92672"&gt;.&lt;/span&gt;register(validate_number)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tool_nr_entry&lt;span style="color:#f92672"&gt;.&lt;/span&gt;config(validate&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;key&amp;#34;&lt;/span&gt;, validatecommand&lt;span style="color:#f92672"&gt;=&lt;/span&gt;(reg, &lt;span style="color:#e6db74"&gt;&amp;#39;%P&amp;#39;&lt;/span&gt;))&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Anschließend wird der eingegebene Wert an einen einfachen Validator übergeben, der lediglich versucht, den Eingabewert in einen Float-Wert umzuwandeln und False zurückgibt, wenn dies einen Fehler erzeugt,&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;validate_number&lt;/span&gt;(entry):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;Helper function that is used in validators to check&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; that the entered keystroke is a number&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; entry &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; float(entry)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;except&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ValueError&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Eine Prüfung ob der Wert plausibel ist erfolgt erst, wenn die Schaltfläche &amp;ldquo;OK&amp;rdquo; gedrückt wird, um die eingegebenen Daten an die Anwendung weiterzuleiten. Bei Überschreitung des Bereichs würde ein Warn-Popup (Modul &lt;code&gt;tkinter.messagebox&lt;/code&gt;) erscheinen, das auf das mögliche Problem hinweist.&lt;/p&gt;&#10;&lt;h3 id="persistenz"&gt;Persistenz&lt;/h3&gt;&#10;&lt;p&gt;Das Persistenzmodul war etwas schwieriger zu implementieren.&#10;Ich entschied mich dafür, ein Element zu erstellen, das C++-Kenner eine &lt;em&gt;statische Klasse&lt;/em&gt; nennen würden, d.h. eine Klasse, die nicht instanziiert werden muss, um eine ihrer Methoden aufzurufen. Diese Klasse hat nur zwei Methoden namens &lt;code&gt;load&lt;/code&gt; und &lt;code&gt;save&lt;/code&gt;, die das tun, was ihr Name andeutet, indem sie Pythons &lt;a href="https://docs.python.org/3/library/pickle.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;pickle&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Modul zur Serialisierung von Objekten verwenden, die ich speichern wollte. In Python habe ich den Dekorator &lt;code&gt;@classmethod&lt;/code&gt; verwendet, um die statische Natur dieser Klasse zu kennzeichnen.&lt;/p&gt;&#10;&lt;p&gt;So einfach kann die Serialisierung von Daten mit den richtigen Werkzeugen werden:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;with&lt;/span&gt; open(app_persistence_path, &lt;span style="color:#e6db74"&gt;&amp;#39;wb&amp;#39;&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; file:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; pickle&lt;span style="color:#f92672"&gt;.&lt;/span&gt;dump([cls&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_tool_list,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cls&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_z_params,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cls&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_xy0],&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; file, protocol&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Als dies zuverlässig funktionierte, fügte ich ein Widget zur Konfiguration der Gravurparameter hinzu. Von Nachteil bei statischen Methoden ist, dass sie nur schlecht unit-testbar sind, weil Dependency Injection hier nicht funktioniert; sie sind ja instanzlos. Es bliebe mir nichts anderes üblich als &amp;ldquo;Fakes&amp;rdquo; zu schreiben, die im Testfalle die echte Speicher- bzw. Ladefunktion ersetzt.&lt;/p&gt;&#10;&lt;h3 id="gravur-parameter"&gt;Gravur-Parameter&lt;/h3&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/qr-codengrave/engrave_params.jpg" alt="Bild: QR-codengrave&amp;#39;s engrave parameter configuration dialog" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Um den G-Code aus meinen QR-Code-Daten zu generieren, benötigte ich nicht nur Werkzeug- und Geschwindigkeits-/Vorschubdaten, sondern ich musste auch wissen, wie tief ich gravieren wollte, wie weit das Werkzeug bei Eilgängen über der Werkstückoberfläche schweben sollte und welche Sicherheitshöhe die CNC für die Rückkehr zum Ursprung oder zur Ausgangsposition verwenden sollte.&lt;/p&gt;&#10;&lt;p&gt;Es wird auf ähnliche Weise wie das Dialogfeld &amp;ldquo;Werkzeugkonfiguration&amp;rdquo; erstellt und verwendet dieselben Mechanismen zur Überprüfung von Typ und Bereich. Der einzige Unterschied besteht darin, dass er nicht über einen Klick auf eine Schaltfläche, sondern direkt über das zugehörige Textfeld gestartet wird, das den aktuellen Wert der Gravurparameter im Hauptfenster anzeigt.&lt;/p&gt;&#10;&lt;h3 id="werkstück-xy-nullpunktverschiebung"&gt;Werkstück XY Nullpunktverschiebung&lt;/h3&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/qr-codengrave/xy0_config.jpg" alt="Bild: QR-codengrave&amp;#39;s Werkstückversatz-Konfigurationsdialog" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Der Konfigurationsdialog für den Werkstückversatz enthält einige Radiobuttons zur Auswahl der gängigsten Werkstückursprünge und eine benutzerdefinierte Option, mit der der Benutzer Werte für den X- und Y-Achsennullpunkt des Werkstücks eingeben kann.&#10;Eine Funktion im Hintergrund berechnet den erforderlichen Versatz relativ zum Startpunkt der Gravierspirale.&lt;/p&gt;&#10;&lt;p&gt;Normalerweise würde ich nicht wollen, dass die grafische Benutzeroberfläche Berechnungen durchführt oder viel Verzweigungslogik verwendet, da dies schwieriger zu verifizieren ist, weil die Test Fixtures damit größer und schwieriger zu warten werden. In diesem Fall habe ich mich jedoch entschieden, die Offset-Berechnung mit Hilfe einer Wertetabelle innerhalb des GUI-Moduls durchzuführen, um zu vermeiden, dass alle GUI-Module zur Plattform und zurück kommunizieren müssen (und das durch 4 Klassen hindurch).&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;_get_xy_offset&lt;/span&gt;(self, offset):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;calculates an XY coordinate offset from a preset point, taking the selected tool diameter into account.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; :param offset the selected offset from the above enum class&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; :returns a XY Point where XY0 is assumed for the engraving.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; qr &lt;span style="color:#f92672"&gt;=&lt;/span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_qr_dimension&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; d &lt;span style="color:#f92672"&gt;=&lt;/span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_tool_diameter&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; offsets &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {Offset&lt;span style="color:#f92672"&gt;.&lt;/span&gt;CENTER: Point((d &lt;span style="color:#f92672"&gt;-&lt;/span&gt; qr) &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, (qr &lt;span style="color:#f92672"&gt;-&lt;/span&gt; d) &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;),&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Offset&lt;span style="color:#f92672"&gt;.&lt;/span&gt;TOPLEFT: Point(d &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;d &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;),&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Offset&lt;span style="color:#f92672"&gt;.&lt;/span&gt;TOPRIGHT: Point(d &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; qr, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;d &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;),&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Offset&lt;span style="color:#f92672"&gt;.&lt;/span&gt;BOTTOMLEFT: Point(d &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, qr &lt;span style="color:#f92672"&gt;-&lt;/span&gt; d &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;),&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Offset&lt;span style="color:#f92672"&gt;.&lt;/span&gt;BOTTOMRIGHT: Point(d &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; qr, qr &lt;span style="color:#f92672"&gt;-&lt;/span&gt; d &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; offset &lt;span style="color:#f92672"&gt;in&lt;/span&gt; offsets:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; offsets[offset]&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="erstes-release-v10"&gt;Erstes Release V1.0&lt;/h2&gt;&#10;&lt;p&gt;Die erste Version ist minimalistisch. Sie erfüllt ihren Zweck, hat aber noch einige Fehler (z.B. eine Fehlermeldung, wenn kein Werkzeug in der Auswahlliste ausgewählt ist) und Unzulänglichkeiten (z.B. ist das Zeichnen eines QR-Codes sehr langsam, beim erneuten Zeichnen kippt der QR-Code ein wenig usw.), aber sie sollte einfach funktionieren und dem Benutzer helfen, die schlimmsten Fehler auf seinem Weg zu vermeiden.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/qr-codengrave/main_application.jpg" alt="Bild: Hauptbildschirm von QR-codengrave" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Ich habe die Ausführung auf meiner CNC-Maschine getestet und zumindest mein Handy ist in der Lage, zu einer Website zu navigieren, indem es einfach die Kamera auf das Werkstück hält 📱.&lt;/p&gt;&#10;&lt;h2 id="automatisierte-integrationstests-für-die-gui-frustrierende-arbeit"&gt;Automatisierte Integrationstests für die GUI: Frustrierende Arbeit&lt;/h2&gt;&#10;&lt;p&gt;Als meine erste Version herauskam, wollte ich einige Tests für die grafische Benutzeroberfläche hinzufügen. Da ich es nicht geschafft habe, dort absolut keine Geschäftslogik zu haben, wollte ich zumindest Bereichsprüfungen sowie die Kommunikation mit der Plattform überprüfen, um sicherzustellen, dass alles wie vorgesehen funktioniert.&lt;/p&gt;&#10;&lt;h3 id="aufgehängt-in-der-messagebox"&gt;Aufgehängt in der Messagebox&lt;/h3&gt;&#10;&lt;p&gt;Zu diesem Zweck schrieb ich einige Tests, die auf meinem lokalen Rechner liefen, mit dem Nachteil, dass, wenn der Test auf einen Warn- oder Fehlerdialog &lt;code&gt;messagebox.showinfo&lt;/code&gt; / &lt;code&gt;messagebox.showerror&lt;/code&gt; stieß, eine manuelle Benutzerinteraktion unbedingt erforderlich war, damit das Fenster geschlossen und der Test fortgesetzt werden konnte.&lt;/p&gt;&#10;&lt;p&gt;Ich habe keine einfache Möglichkeit gefunden, dieses Problem zu umgehen, da die Messagebox als unabhängiges Widget agiert, das nicht einfach manipuliert werden kann. Auch meine Versuche, einen Tastendruck als Seiteneffekt aus dem Test heraus auszulösen, waren nicht von Erfolg gekrönt (und wären am Ende wohl auch keine saubere Lösung gewesen).&lt;/p&gt;&#10;&lt;p&gt;Meine Lösung nach ein paar Stunden gemurmelten Fluchens ist, dass die Messagebox-Widgets in die GUI-Klassen injiziert werden, die sie verwenden:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@patch&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;src.gui.gui_tool_manage.GuiToolManager&amp;#39;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;setUp&lt;/span&gt;(self, mock_guitoolmanager):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tk&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Tk() &lt;span style="color:#75715e"&gt;# required to have tk variables properly instantiated&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mock_guitoolmanager &lt;span style="color:#f92672"&gt;=&lt;/span&gt; mock_guitoolmanager&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mock_msg &lt;span style="color:#f92672"&gt;=&lt;/span&gt; MsgBox()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mock_msg&lt;span style="color:#f92672"&gt;.&lt;/span&gt;showinfo &lt;span style="color:#f92672"&gt;=&lt;/span&gt; MagicMock()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Persistence&lt;span style="color:#f92672"&gt;.&lt;/span&gt;set_mock_msgbox(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mock_msg)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;config_tool &lt;span style="color:#f92672"&gt;=&lt;/span&gt; GuiConfigureTool(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mock_guitoolmanager, self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mock_msg, {&lt;span style="color:#e6db74"&gt;&amp;#39;padx&amp;#39;&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;pady&amp;#39;&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;})&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Auf diese Weise kann ich &lt;code&gt;mock_msg&lt;/code&gt; in die Gui-Klasse werfen, so dass nicht die echte showinfo genommen wird, sondern das Mock weiß, ob er wie vorgesehen aufgerufen wurde. Ich könnte ihr zur Testvorrichtung hinzufügen, so dass mein Testcode selbst &lt;a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself" target="_blank" rel="noopener noreferrer" class="external-link"&gt;DRY&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; bleiben könnte.&lt;/p&gt;&#10;&lt;p&gt;Es fügt dem Konstruktor mehr Argumente hinzu und delegiert die Instanziierung an eine Klasse höherer Ebene, aber die Verwendung innerhalb der zu testenden Klasse bleibt trivial. Im Einsatz unter Realbedingungen agiert die Schnittstelle wie ein Proxy, der dann die echte &lt;code&gt;showinfo&lt;/code&gt;-Methode des Messagebox Objektes aufruft.&lt;/p&gt;&#10;&lt;h3 id="probleme-mit-statischen-klassen"&gt;Probleme mit statischen Klassen&lt;/h3&gt;&#10;&lt;p&gt;Da die Persistenzklasse, die ich &lt;a href="https://blog.schallbert.de/projects/qr-codengrave/#persistenz"&gt;oben&lt;/a&gt; erwähnt habe, &lt;em&gt;statisch&lt;/em&gt; ist, d.h. keine Instanziierung erfordert, hatte ich das nächste Problem von Anfang an in meinen Code eingebaut: Ein Integrationstest würde nun versuchen, in eine tatsächliche &amp;ldquo;persistence.dat&amp;rdquo;-Datei zu schreiben, eine Abhängigkeit, die ich nicht haben wollte, insbesondere nicht in der kontinuierlichen Integration (CI) - denn dort würde das Programm die Rechte für das Schreiben auf Festplatte nicht besitzen.&lt;/p&gt;&#10;&lt;p&gt;Also habe ich Persistence in die Haupt-GUI-Datei verschoben, die nicht getestet werden würde, und folgte damit &lt;a href="https://blog.stevensanderson.com/2009/11/04/selective-unit-testing-costs-and-benefits/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Steven Andersons großartigem Blogbeitrag&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Die Haupt-GUI sollte ohnehin nur deklarativen Code enthalten, und die geladenen Daten werden dann in die Klassen kaskadiert, die sie verwenden, anstatt in die Persistenzklasse, die die eigentliche Lade-/Speicheroperation durchführt.&lt;/p&gt;&#10;&lt;h2 id="zukünftige-versionen"&gt;Zukünftige Versionen&lt;/h2&gt;&#10;&lt;p&gt;Folgendes ist für spätere Releases geplant:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Die Zeichnungsgeschwindigkeit des QR-Codes muss erhöht werden&lt;/li&gt;&#10;&lt;li&gt;Grafikfehler (Skalierung, Kippen) und unscharfe Ränder des Zeichenbildschirms sollen behoben werden&lt;/li&gt;&#10;&lt;li&gt;CNC-Bahnen sollen optimiert werden, um die Fertigungszeit zu verringern&lt;/li&gt;&#10;&lt;li&gt;Fehlerbehebungen&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Zumindest wenn es welche gibt; mal sehen, wie oft ich dieses Tool benutzen werde. Die Dinge, die mich am meisten stören, werden wahrscheinlich zuerst behoben werden.&lt;/p&gt;&#10;&lt;p&gt;Ach ja, noch etwas: Danke für&amp;rsquo;s Lesen des gesamten Artikels. Zur Belohnung gibt es eine Kopie &lt;a href="https://github.com/Schallbert/QR-codengrave/releases/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;QR-codengrave&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; kostenlos zum Herunterladen. Es kann für den privaten Gebrauch gern verwendet werden. Falls Sie eine kommerzielle Nutzung planen, lassen Sie es mich bitte im Voraus wissen.&lt;/p&gt;&#10;</description></item><item><title>Tonuino - alternative Firmware</title><link>https://blog.schallbert.de/projects/tonuino/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/tonuino/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/tonuino/tonuino-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Tonuino as a Bird&amp;#39;s house"&#10; title="Tonuino - alternative Firmware" /&gt;&#10;&lt;h2 id="projekt-steckbrief"&gt;Projekt-Steckbrief&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Difficulty: Experte 5/5&lt;/li&gt;&#10;&lt;li&gt;Kosten: 0€&lt;/li&gt;&#10;&lt;li&gt;Zeitaufwand: ~400h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="galerie"&gt;Galerie&lt;/h2&gt;&#10;&lt;section class="hugo-gallery"&gt;&#10; &lt;div class="hugo-gallery__frame"&gt;&#10; &lt;div class="hugo-gallery__grid" role="list"&gt;&lt;a class="hugo-gallery__thumb" href="#gallery-full-0" aria-label="Image: NFC tags have been woven into little birds made of felt. They can be positioned inside the bird&amp;#39;s house to play music."&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/tonuino/birdshouse_front.jpg" alt="Image: NFC tags have been woven into little birds made of felt. They can be positioned inside the bird&amp;#39;s house to play music." class="hugo-gallery__thumb-img" loading="lazy" /&gt;&#10; &lt;/a&gt;&lt;a class="hugo-gallery__thumb" href="#gallery-full-1" aria-label="Image: birdshouse opened. Its contents: An RFID reader, MP3 player, speaker, charger socket, a powerbank, one multicolor-LED and the clickEncoder as human-machine interface."&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/tonuino/birdshouse_inside.jpg" alt="Image: birdshouse opened. Its contents: An RFID reader, MP3 player, speaker, charger socket, a powerbank, one multicolor-LED and the clickEncoder as human-machine interface." class="hugo-gallery__thumb-img" loading="lazy" /&gt;&#10; &lt;/a&gt;&lt;a class="hugo-gallery__thumb" href="#gallery-full-2" aria-label="Image: Guitar Amp design: All components are built in an old guitar amp model. Its battery, amplifier and speaker have been reused for Tonuino, and some buttons have been added to control the mp3 player. Cards are placed on top of the amp to be read and select content to be played."&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/tonuino/guitaramp_top.jpg" alt="Image: Guitar Amp design: All components are built in an old guitar amp model. Its battery, amplifier and speaker have been reused for Tonuino, and some buttons have been added to control the mp3 player. Cards are placed on top of the amp to be read and select content to be played." class="hugo-gallery__thumb-img" loading="lazy" /&gt;&#10; &lt;/a&gt;&lt;a class="hugo-gallery__thumb" href="#gallery-full-3" aria-label="Image: insides of the guitar amp: An Arduino nano controls all functions. A Bistable relay keeps the system alive as long as it plays music and switches off automatically when done to save battery power."&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/tonuino/guitaramp_inside.jpg" alt="Image: insides of the guitar amp: An Arduino nano controls all functions. A Bistable relay keeps the system alive as long as it plays music and switches off automatically when done to save battery power." class="hugo-gallery__thumb-img" loading="lazy" /&gt;&#10; &lt;/a&gt;&lt;/div&gt;&lt;div class="hugo-gallery__caption"&gt;Die alternative Tonuino-Firmware im Einsatz.&lt;/div&gt;&lt;/div&gt;&#10;&lt;/section&gt;&#10;&#10; &lt;div class="hugo-gallery__full" aria-live="polite"&gt;&lt;figure id="gallery-full-0" class="hugo-gallery__figure"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/tonuino/birdshouse_front.jpg" alt="Image: NFC tags have been woven into little birds made of felt. They can be positioned inside the bird&amp;#39;s house to play music." class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__figcaption"&gt;NFC-Tags sind Vögel, die zum Abspielen ins Häuschen gesetzt werden&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure id="gallery-full-1" class="hugo-gallery__figure"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/tonuino/birdshouse_inside.jpg" alt="Image: birdshouse opened. Its contents: An RFID reader, MP3 player, speaker, charger socket, a powerbank, one multicolor-LED and the clickEncoder as human-machine interface." class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__figcaption"&gt;Anwendungsbeispiel mit Microcontroller unter Verwendung meiner ClickEncoder-Bibliothek&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure id="gallery-full-2" class="hugo-gallery__figure"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/tonuino/guitaramp_top.jpg" alt="Image: Guitar Amp design: All components are built in an old guitar amp model. Its battery, amplifier and speaker have been reused for Tonuino, and some buttons have been added to control the mp3 player. Cards are placed on top of the amp to be read and select content to be played." class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__figcaption"&gt;Anwendungsbeispiel mit Arduino in einem Gitarrenverstärker&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure id="gallery-full-3" class="hugo-gallery__figure"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/tonuino/guitaramp_inside.jpg" alt="Image: insides of the guitar amp: An Arduino nano controls all functions. A Bistable relay keeps the system alive as long as it plays music and switches off automatically when done to save battery power." class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__figcaption"&gt;Gesteuert wird der Player in diesem Fall mit drei Tasten.&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&#10;&lt;/section&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://raw.githubusercontent.com/isocpp/logos/64ef037049f87ac74875dbe72695e59118b52186/cpp_logo.svg" alt="Image: C&amp;#43;&amp;#43; Logo" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;In disem Projekt entstand eine Software für eine NFC-Tag gesteuerte Jukebox. Sie ist für das Arduino-Framework optimiert und verbindet die Hardware eines Mp3-Players (mit SD-Karte), ein NFC-Tag-Lesegerät und ein paar Bedienelemente.&lt;/p&gt;&#10;&lt;p&gt;Ein NFC-Tag wird mit einem Ordner auf der SD-Karte verknüpft, welcher Audiodateien enthält. Das Tag speichert auch Informationen über den gewünschten Wiedergabemodus für dieses Album, z. B. &amp;ldquo;Zufällig&amp;rdquo;. Sobald ein verknüpfter Tag in die Nähe der Jukebox gebracht wird, wird der konfigurierte Ordner im entsprechenden Wiedergabemodus abgespielt.&#10;Mit den Bedienelementen kann der Titel ausgewählt, die Lautstärke geändert und sogar durch ein Sprachmenü navigiert werden, was das Löschen, Verknüpfen und Konfigurieren eines Tags sowie Sperren/Entsperren der Eingabetasten ermöglicht.&lt;/p&gt;&#10;&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;&#10;&lt;p&gt;Idee und fantastische Umsetzung hatte ich auf &lt;a href="https://www.voss.earth/tonuino" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Thorsten Voß&amp;rsquo; blog&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; gesehen. Es begann mit der Funktionalität (&lt;a href="https://blog.schallbert.de/projects/encoder/"&gt;encoder support&lt;/a&gt;), die ich hinzufügen wollte. Leider war der ursprüngliche Code ein zusammenhängender Block mit mehreren tausend Zeilen Code und für mich schlechter Lesbarkeit, so dass es mir schwer fiel, meine Änderungen einzubringen.&lt;/p&gt;&#10;&lt;p&gt;Also habe ich den Code von Grund auf neu geschrieben, um eine bessere Struktur, Lesbarkeit, Wartbarkeit und Erweiterbarkeit zu erreichen. Parallel dazu las ich einige Bücher über objektorientiertes Design in &lt;code&gt;C++&lt;/code&gt;, &lt;a href="https://en.wikipedia.org/wiki/Robert_C._Martin" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Clean Code&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und &lt;a href="https://en.wikipedia.org/wiki/Software_design_pattern" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Software Design Patterns&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und wandte einige davon dort an, wo ich sie für geeignet hielt. Auf diese Weise konnte ich sowohl die Modularität als auch die Lesbarkeit des Codes verbessern und habe außerdem gelernt, wie man Code in einem &amp;ldquo;größeren&amp;rdquo; Projekt schreibt.&lt;/p&gt;&#10;&lt;h2 id="schwierigkeiten"&gt;Schwierigkeiten&lt;/h2&gt;&#10;&lt;p&gt;Dies war mein erstes eigenes &lt;code&gt;C++&lt;/code&gt;-Projekt, und vielleicht war es für den Anfang ein wenig zu groß. All die für mich neuen Konzepte, das Schreiben an eine Schnittstelle, die erstmalige Verwendung von &lt;a href="https://platformio.org" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Platformio als IDE&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, die Verwendung des &lt;a href="https://google.github.io/googletest" target="_blank" rel="noopener noreferrer" class="external-link"&gt;googletest unit test framework&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, die Einsatz von Coding Patterns wie [factory](https://www. oodesign.com/factory-pattern.html) oder &lt;a href="https://en.wikipedia.org/wiki/Dependency_injection" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Dependency Injection&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; kostete mich Abend um Abend über fast ein Jahr, bis ich dieses Projekt schließlich fertigstellen konnte. Und es gibt immer noch etwas zu überarbeiten und zu verbessern.&lt;/p&gt;&#10;&lt;h2 id="übersicht"&gt;Übersicht&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Unit-Test-Suite mit über 250 Testfällen&lt;/li&gt;&#10;&lt;li&gt;Serielle Debug-Ausgabe konfigurierbar&lt;/li&gt;&#10;&lt;li&gt;Lose gekoppelte, objektorientierte Architektur in &lt;code&gt;C++&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Einzelne Module mit klaren Aufgaben, skalierbar und leicht zugänglich für zukünftige Funktionen&lt;/li&gt;&#10;&lt;li&gt;benutzerdefinierte Hardware-Abstraktionsschicht; der größte Teil des Codes sollte ohne Änderungen auf andere MCUs portierbar sein&lt;/li&gt;&#10;&lt;li&gt;eigenes Dependency Injection Framework (Loader Klasse)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="funktionen"&gt;Funktionen&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Konfigurierbare Benutzereingabe (Tasten oder Encoder)&lt;/li&gt;&#10;&lt;li&gt;Auto-Poweroff, wenn für eine konfigurierbare Zeit keine Taste gedrückt wird&lt;/li&gt;&#10;&lt;li&gt;Einschalten durch Drücken der &amp;ldquo;Play&amp;rdquo;-Taste&lt;/li&gt;&#10;&lt;li&gt;Autoplay-Funktion&lt;/li&gt;&#10;&lt;li&gt;Status-Led-Funktion&lt;/li&gt;&#10;&lt;li&gt;Sperren/Entsperren der Benutzereingabe&lt;/li&gt;&#10;&lt;li&gt;Mehrere Wiedergabemodi [Album, Zufall, Speichern des Titelfortschritts, Nur ein Titel] pro Nfc-Tag verfügbar&lt;/li&gt;&#10;&lt;li&gt;Sprachmenüs zum Verknüpfen oder Löschen von Nfc-Tags&lt;/li&gt;&#10;&lt;li&gt;Optimiert für Batterieanwendungen (z.B. Powerbank) unter Verwendung von Ruhezuständen&lt;/li&gt;&#10;&lt;li&gt;Niedriger Stromverbrauch &lt;code&gt;@5V&lt;/code&gt;: &lt;code&gt;~40mA&lt;/code&gt; im Leerlauf, &lt;code&gt;~75mA&lt;/code&gt; bei mittlerer Lautstärke&lt;/li&gt;&#10;&lt;li&gt;Konfigurationsdatei für Startlautstärke, Einschlaf-Timer etc.&lt;/li&gt;&#10;&lt;li&gt;Automatische Wiederherstellung bei Eingabeaufforderung, die niemand bedient&lt;/li&gt;&#10;&lt;li&gt;Sprachansagen für die häufigsten Fehlermeldungen&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="nicht-enthaltene-funktionen"&gt;Nicht enthaltene Funktionen&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Keine Erkennung des Ladezustands der Powerbank&lt;/li&gt;&#10;&lt;li&gt;Kein Konfigurationsmenü (Startlautstärke, Dauer des Einschlaf-Timers, Standby-Dauer usw.)&lt;/li&gt;&#10;&lt;li&gt;&amp;hellip;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="dokumentation"&gt;Dokumentation&lt;/h2&gt;&#10;&lt;p&gt;Das Projekt ist für das Arduino Framework erstellt - getestet auf einem Arduino nano Board - unter Verwendung der &lt;a href="https://platformio.org" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PlatformIO IDE&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&#10;Die folgenden Abschnitte zeigen das Design.&lt;/p&gt;&#10;&lt;h3 id="projekt-modulübersicht"&gt;Projekt Modulübersicht&lt;/h3&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Ordnername in &lt;code&gt;/lib&lt;/code&gt;&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Zweck&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Arduino&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;minimalistische hardwarenahe Implementierungen, nicht Unit-testbar&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Arduino_HardwareAbstraction&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Hardware-Abstraktion, um Portabilität und Testen zu ermöglichen&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Config&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Systemkonfigurationsparameter&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Folder&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Geschäftslogik für Wiedergabeliste und Wiedergabemodus&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Loader&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Dependency Injection Framework&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;MessageHandler&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Systemnachrichten und Debug-Framework&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Mp3&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Mp3-Steuerung (Status, Ordner, Sprachausgabe, Anzeigen)&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Nfc&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Tag-Steuerung (Status, Lesen, Schreiben, Löschen)&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;PowerManager&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Steuerung von Status-LED und Ruhezustand abhängig vom Systemstatus&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Tonuino&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Haupt-Task-Scheduler&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;UserInput&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Verarbeitung von Tasten- oder Encoder-Eingaben&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Utilities&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Timer, Led-Steuerung, Pin-Steuerung&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;VoiceMenu&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Geschäftslogik für das Menü Link / Löschen / Konfiguration&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="verwendete-externe-bibliotheken"&gt;Verwendete externe Bibliotheken&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://platformio.org/lib/show/131/TimerOne" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Arduino&amp;rsquo;s TimerOne&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://platformio.org/lib/show/7212/SoftwareSerial" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Arduino&amp;rsquo;s SoftwareSerial&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://platformio.org/lib/show/11808/ClickEncoder" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Schallbert&amp;rsquo;s ClickEncoder&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://platformio.org/lib/show/1561/DFPlayer%20Mini%20Mp3%20by%20Makuna" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Makuna&amp;rsquo;s DFMini Mp3&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://platformio.org/lib/show/63/MFRC522" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Miguel Balboa&amp;rsquo;s MFRC522&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Automatische Installation durch Platformios &amp;ldquo;Library Dependency Finder&amp;rdquo; beim erstem Kompiliervorgang&lt;/p&gt;&#10;&lt;h3 id="klassendiagramme"&gt;Klassendiagramme&lt;/h3&gt;&#10;&lt;p&gt;Klassendiagramme wären zu viel nicht-automatisierte Arbeit gewesen. Stattdessen habe ich das hier vorbereitet:&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://raw.githubusercontent.com/Schallbert/Tonuino_alternativeFirmware/master/docs/ProjectModulesOverview.png" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Software-Modulübersicht&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;Sie sollte ein genaues Verständnis dafür vermitteln, wie die Softwaremodule zusammenwirken und welche APIs die Module anbieten.&lt;/p&gt;&#10;&lt;h2 id="los-gehts"&gt;Los geht&amp;rsquo;s!&lt;/h2&gt;&#10;&lt;p&gt;Zuerst muss mein &lt;a href="https://github.com/Schallbert/Tonuino_alternativeFirmware" target="_blank" rel="noopener noreferrer" class="external-link"&gt;repository&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, geklont werden - entweder per &lt;code&gt;git clone&lt;/code&gt;, Download als &lt;code&gt;zip&lt;/code&gt;-Datei oder per &lt;code&gt;https&lt;/code&gt;. Das &lt;code&gt;README.md&lt;/code&gt; Dokument bietet einen leichten Einstieg. Ich empfehle die Verwendung von PlatformIO für dieses Projekt.&#10;In den folgenden Abschnitten sind die Teilfunktionen des Programms erläutert, und wie deren korrekte Funktion sichergestellt wird.&lt;/p&gt;&#10;&lt;h2 id="testen"&gt;Testen&lt;/h2&gt;&#10;&lt;h3 id="unit-tests"&gt;Unit-Tests&lt;/h3&gt;&#10;&lt;p&gt;Unit-Tests wurden mit dem &lt;code&gt;gtest&lt;/code&gt; C++ Unit-Test-Framework geschrieben. Sie befinden sich im Ordner &lt;code&gt;test/desktop&lt;/code&gt;. Zu beachten ist, dass &lt;em&gt;googletest&lt;/em&gt; die Installation von &lt;code&gt;gcc&lt;/code&gt; mit einigen Bibliotheken voraussetzt. Die Anleitung dazu findet man &lt;a href="https://blog.schallbert.de/projects/platformio_gtestgmock/"&gt;in einem meiner anderen Projekte&lt;/a&gt;.&#10;Sobald diese Voraussetzungen erfüllt sind und ein einfacher &lt;code&gt;ASSERT_TRUE(false);&lt;/code&gt;-Test mit der gtest-Umgebung erwartungsgemäß fehlschlägt, können die Unit-Tests des Projekts erstellt und mit &lt;code&gt;pio test -e desktop -f desktop&lt;/code&gt;, in der PlatformIO CLI (Terminal) ausgeführt werden.&lt;/p&gt;&#10;&lt;h3 id="akzeptanztests"&gt;Akzeptanztests&lt;/h3&gt;&#10;&lt;p&gt;Obwohl diese Tests automatisiert werden könnten, ist es viel einfacher, diese Tests von Hand durchzuführen, nachdem der Mikrocontroller programmiert und alle elektronischen Komponenten zusammengesteckt wurden.&#10;Jede Zeile in den folgenden Tabellen ist ein eigenständiger Testfall. Die Testsuiten (&amp;ldquo;Überschrift Name&amp;rdquo;) haben eine &lt;strong&gt;System Pre:&lt;/strong&gt; Eigenschaft, die vor der Ausführung der einzelnen Testfälle wiederhergestellt werden muss. Wenn die Erwartungsklausel erfüllt ist, ist der Test &lt;strong&gt;BESTANDEN&lt;/strong&gt;. Ich habe die Tests auf Englisch belassen, weil sie recht eng mit dem Code korrespondieren.&lt;/p&gt;&#10;&lt;h4 id="switching-on"&gt;Switching ON&lt;/h4&gt;&#10;&lt;p&gt;System Pre: System is OFF&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Action&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Expectation&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;press Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;LED flashes slowly?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;press Play/pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Welcome prompt plays?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h4 id="switching-off"&gt;Switching OFF&lt;/h4&gt;&#10;&lt;p&gt;System Pre: System is ON&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Action&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Expectation&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Playback on pause, no button input&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;System switches off after a time?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;System shutdown&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Farewell prompt plays?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;System shutdown&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;LED switched off?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h4 id="play-help-prompt"&gt;Play Help Prompt&lt;/h4&gt;&#10;&lt;p&gt;System Pre: System is ON&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Action&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Expectation&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Long press Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Help prompt plays?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Help prompt playing&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Can be interrupted with any button press?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h4 id="behavior-without-tag"&gt;Behavior without Tag&lt;/h4&gt;&#10;&lt;p&gt;System Pre: System is ON, no Tag present&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Action&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Expectation&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;press Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;prompts &amp;ldquo;couldn&amp;rsquo;t find track&amp;rdquo; error?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;press Next button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;prompts &amp;ldquo;couldn&amp;rsquo;t find track&amp;rdquo; error?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;press Prev button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;prompts &amp;ldquo;couldn&amp;rsquo;t find track&amp;rdquo; error?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;doubleclick Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Delete Menu prompt played?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h4 id="behavior-with-tag"&gt;Behavior with Tag&lt;/h4&gt;&#10;&lt;p&gt;System Pre: System is ON, linked Tag available&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Precondition&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Action&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Expectation&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;no Tag placed&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;place known Tag&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;starts Playback of correct Folder?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;active playback&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;press Next button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;plays next track?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;active playback&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;press Next button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;next track in accordance with selected playMode of Folder?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;active playback&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;long press Next button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;increases volume?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;active playback&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;press Prev button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;plays previous track?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;active playback&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;press Prev button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;previous track in accordance with selected playMode of Folder?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;active playback&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;long press Prev button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;decreases volume?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;active playback&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;press Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;pauses track?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;paused playback&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;press Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;resumes track?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;active playback&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;doubleclick Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Lock prompt played?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;active playback&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;doubleclick Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Playback resumes after Lock prompt played?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;active playback&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;doubleclick Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;locks button input?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;locked button input&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;doubleclick Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Unlock prompt played?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;locked button input&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;doubleclick Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Playback resumes after Unlock prompt played?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;locked button input&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;doubleclick Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;unlocks button input?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;paused playback&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;doubleclick Play/Pause button&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Playback resumes after Lock prompt played?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h4 id="behavior-with-unlinked-tag"&gt;Behavior with unlinked Tag&lt;/h4&gt;&#10;&lt;p&gt;System Pre: System is ON&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Action&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Expectation&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;place unlinked Tag&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;plays LinkMenu Prompt?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;navigate through LinkMenu&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;plays Configuration Success Prompt?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;place Tag again&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;plays linked folder?&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h2 id="hardware"&gt;Hardware&lt;/h2&gt;&#10;&lt;h3 id="materialliste"&gt;Materialliste&lt;/h3&gt;&#10;&lt;p&gt;Ich habe versucht mit möglichst wenigen Einzelteilen auszukommen. Für ein Projekt dieser Größe ist die Liste denke ich recht kurz geblieben.&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Menge&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Artikel&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Zweck&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Arduino (z.B. Nano)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Gehirn&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Dfplayer Mini Mp3 Player&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Mund&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Micro SD Karte&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Speicher&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;MFRC522 Nfc-Leser&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Psi-Sense&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Bistabiles Relais 5V, z.B. HFE20&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Kaffee - kein Schlaf&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Dioden z.B. 1n4007&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Venenklappe&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;PNP-Transistor z. B. BC327&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Kaffeemaschinenschalter&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Widerstände 1k&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Blutstromaufbereiter&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Widerstand 220&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Blinkstärke dimmen&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;LED, Farbe nach Wahl&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;blinken&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Powerbank/ 5V Versorgung&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Lebensmittel&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;3&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Drucktasten, z.B. Cherry MX Tasten&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Drucksensoren&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Lautsprecher, z.B. 5W@4Ohms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Stimmbänder&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;USB-A-Stecker&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Strohhalm&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Gehäuse&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Körper&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="zusätzliche-bauteile-und-tipps"&gt;Zusätzliche Bauteile und Tipps&lt;/h3&gt;&#10;&lt;p&gt;Überbrückungsdrähte, Platine, Buchsen und Verbrauchsmaterial nach eigenem Ermessen&#10;Wie beim &amp;ldquo;Original&amp;rdquo;-Projekt; alternativ einen Drehgeber anstelle der Drucktasten kaufen und das Projekt so konfigurieren, dass er statt der Taster ausgelesen wird.&#10;Da das System für den Batteriebetrieb optimiert ist, nehmen Sie ein bi-stabiles Relais oder einen JFET-Transistor (mit niedriger Gate-Spannung und niedrigem Source-Drain-Spannungsabfall) für die KeepAlive-Funktionalität, die den zusätzlichen Strom durch die ständig eingeschalteten Relaisspulen vermeidet. Eine billige Powerbank reicht für viele Stunden Betrieb. Alternativ können z.B. 3xAA-Batterien verwendet werden - allerdings macht der Dfmini-Player ordentlich Lärm, wenn die Spannung zu sehr einbricht. Er kann sogar bei wiederholten Neustarts durch Stromstöße zerstört werden, also Vorsicht bei nachlassender Akkuladung.&lt;/p&gt;&#10;&lt;h3 id="schaltplan"&gt;Schaltplan&lt;/h3&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://raw.githubusercontent.com/Schallbert/Tonuino_alternativeFirmware/master/docs/CircuitDiagram.jpg" alt="Stromlaufplan" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;</description></item></channel></rss>