<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Projects on Schallberts Blog</title><link>https://blog.schallbert.de/projects/</link><description>Recent content in Projects on Schallberts Blog</description><generator>Hugo</generator><language>de</language><lastBuildDate>Sun, 06 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.schallbert.de/projects/index.xml" rel="self" type="application/rss+xml"/><item><title>Umzug von Jekyll zu Hugo</title><link>https://blog.schallbert.de/projects/migrating-jekyll-to-hugo/</link><pubDate>Sun, 06 Sep 2026 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/migrating-jekyll-to-hugo/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/migrating-hugo-to-jekyll/hugo-jekyll-build-compare-cover.avif"&#10; class="post-cover"&#10; alt="Image: Hugo console build output. It shows a full build time below 8sec. (~70% reduction over build time with Jekyll)"&#10; title="Umzug von Jekyll zu Hugo" /&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;Zeit ~20h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;In diesem Projekt beschreibe ich meinen Umzug von Jekyll mit der Optik von Minimal Mistakes zu Hugo (Theme Terminal).&lt;/p&gt;&#10;&lt;h2 id="warum-überhaupt-wechseln"&gt;Warum überhaupt wechseln?&lt;/h2&gt;&#10;&lt;p&gt;Schon seit langem war ich nicht mehr vollständig zufrieden mit dem Bauen meiner Website per Jekyll. Der Auslöser für den Umstieg war, dass ich eines Tages plötzlich wegen eines Kompatibilitätsproblemes nicht mehr lokal bauen konnte. Ein &lt;code&gt;bundle update --conservative&lt;/code&gt; oder &lt;code&gt;bundle install&lt;/code&gt; wie üblich half nicht. Auch das manuelle Nachinstallieren der verursachenden Pakete per &lt;code&gt;gem install commonmarker-0.23.12&lt;/code&gt; oder &lt;code&gt;gem install posix-spawn -v 0.3.15 -- --with-cflags=\&amp;quot;-Wno-incompatible-function-pointer-types\&amp;quot;&lt;/code&gt; brachten keine Abhilfe.&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/migrating-hugo-to-jekyll/jekyll-update-bundle-installer-error.avif" alt="Image: Bundler dependency tree showing the error: Failed to build gem native extension. Multiple errors like this occurred lately when I tried building my site." /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Ein Update von &lt;em&gt;Ruby&lt;/em&gt; und &lt;em&gt;Jekyll&lt;/em&gt; selbst brachte ebenfalls keinen Erfolg. Und überhaupt: Die Bauzeit mit Jekyll ist inzwischen ziemlich lang geworden. Ich bin durch meinen &lt;a href="https://blog.schallbert.de/projects/move-blog-to-own-server/"&gt;Umzug der Seite von Github Pages auf einen eigenen Server&lt;/a&gt; nicht mehr an Jekyll gebunden, was einen Wechsel attraktiver macht.&lt;/p&gt;&#10;&lt;h3 id="weitere-schwierigkeiten-mit-jekyll"&gt;Weitere Schwierigkeiten mit Jekyll&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;CI/CD: Meine Deploy-Pipeline war vor Monaten mit ähnlichen Fehlern ausgefallen, sodass ich auf eine ältere Jekyll-Version zurückgreifen musste. Damit vergrößert sich technische Schuld und Angriffsfläche meiner Actions. Ich müsste auf die Suche nach einer gut gewarteten Containerlösung eines anderen Anbieters wechseln. Suche und Einrichtung würden zusätzliche Zeit erfordern.&lt;/li&gt;&#10;&lt;li&gt;RSS-Feed: Ich möchte zu meiner Website gern einen RSS-Feed bereitstellen. Meine Experimente mit Jekyll zeigten jedoch, dass nur &amp;ldquo;Posts&amp;rdquo; und keine &amp;ldquo;Announcements&amp;rdquo; inkludiert werden. Das wollte ich beheben.&lt;/li&gt;&#10;&lt;li&gt;Bildformate: Cover-Bilder (Thumbnails) für Posts werden im RSS-Feed durch Jekyll eigentlich mit gebaut. Durch meinen Umstieg von &lt;code&gt;.jpg&lt;/code&gt; auf das platzsparendere &lt;code&gt;.avif&lt;/code&gt; funktionierte das nun leider nicht mehr. Eine Lösung habe ich trotz mehrerer Anläufe nicht durch den Pull Request Review bekommen.&lt;/li&gt;&#10;&lt;li&gt;Icons: Das von mir verwendete Theme nutzt &lt;em&gt;Font Awesome&lt;/em&gt; zur Darstellung von Icons und ist insgesamt für eine statische Website recht groß. Ich wollte aber nichts aus dem Netz nachladen und habe die Bibliothek daher lokal gespeichert. Daher war mein Plan, nur die wirklich benötigten Icons heraussuchen und den Rest zu löschen. Mit dem Umstieg auf Hugo ist dies nicht mehr notwendig.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="die-chancen"&gt;Die Chancen&lt;/h3&gt;&#10;&lt;p&gt;Von Hugo verspreche ich mir schnellere Bauzeiten, eine vereinfachte CI/CD ohne Nachinstallieren von Bundler, Ruby etc. und die Behebung der oben beschriebenen Schwierigkeiten. Das Abhängigkeits- und Paketmanagement von Hugo unterscheidet sich grundlegend von Jekyll: Zusätzliche Inhalte werden per &lt;a href="https://git-scm.com/book/en/v2/Git-Tools-Submodules" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Git Submodules&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; eingebunden und das Abhängigkeismanagement nutzt statt der &lt;a href="https://bundler.io/man/gemfile.5.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Gemfile&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; schlicht &lt;a href="https://instagit.com/gohugoio/hugo/hugo-dependency-management-go-modules/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Go modules&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="das-migrationswerkzeug"&gt;Das Migrationswerkzeug&lt;/h2&gt;&#10;&lt;p&gt;Die Ordnerstrukturen der Inhalte unterscheidet sich deutlich zwischen &lt;em&gt;Jekyll&lt;/em&gt; und &lt;em&gt;Hugo&lt;/em&gt;. Ich nutze daher ein in Hugo enthaltenes &lt;a href="https://gohugo.io/commands/hugo_import_jekyll/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Importwerkzeug&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, welches mir zumindest die Artikel unter &lt;code&gt;/posts&lt;/code&gt; korrekt importiert.&lt;/p&gt;&#10;&lt;p&gt;Alle spezifischen Inhalte wie Übersichtsseiten &lt;code&gt;/tags, /pages/error, /pages/legal&lt;/code&gt; und Seiten, wo Projekte und Ankündigungen abgelegt sind &lt;code&gt;/_announcements, /_projects&lt;/code&gt;, werden allerdings nicht mit umgezogen und müssen manuell kopiert und modifiziert werden.&lt;/p&gt;&#10;&lt;h3 id="anpassen-der-front-matter"&gt;Anpassen der Front Matter&lt;/h3&gt;&#10;&lt;p&gt;Viele von mir gemachten Anpassungen in Jekyll funktionieren in Hugo entweder nicht richtig oder führen zum Abbruch des Bauvorganges - die &lt;strong&gt;Front Matter&lt;/strong&gt; einer jeden Markdown-Datei muss also angepasst und teils bereinigt 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-yml" data-lang="yml"&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;# Alt: So kann die Front matter einer Übersichtsseite in Jekyll aussehen&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;lang&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;de&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;title&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Beitragsarchiv&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;subtitle&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Diese Sammlung enthält alle meine Beiträge&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;layout&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;collection&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;collection&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;posts&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;permalink&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;/posts-archive/&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;entries_layout&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;grid&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;classes&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;wide&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;author_profile&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&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;In meinem Falle liegen einige Theme-abhänigen Parameter im Frontmatter vor wie &lt;code&gt;layout, classes, author_profile&lt;/code&gt;. Der &lt;code&gt;lang&lt;/code&gt;-Parameter aus meiner Internationalisierungserweiterung in Jekyll ist nicht einmal korrekte Hugo-Syntax: es erwartet die angezeigte Sprache entweder als Feld unter &lt;code&gt;params: lang&lt;/code&gt; oder Ordnerweit. Auch andere, an den eigenen Geschmack angepasste Parameter können definiert und unter &lt;code&gt;params&lt;/code&gt; mitgegeben werden.&lt;/p&gt;&#10;&lt;p&gt;Permalinks finden gar keinen Eingang mehr in die Front matter, stattdessen findet die Konfiguration &lt;a href="https://gohugo.io/configuration/permalinks/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;zentral statt&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Eine &lt;a href="https://gohugo.io/content-management/front-matter/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Hugo-kompatible front matter&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; kann so aussehen:&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-yml" data-lang="yml"&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:#f92672"&gt;title&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Beitragsarchiv&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;description&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Diese Sammlung enthält alle meine Beiträge&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;layout&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;collection&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;h2 id="konfiguration-des-theme"&gt;Konfiguration des Theme&lt;/h2&gt;&#10;&lt;p&gt;Eine Basis-Konfiguration meines Themes kann ich mit folgendem Kommando installieren:&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-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ blog git:&lt;span style="color:#f92672"&gt;(&lt;/span&gt;main&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ✗ git submodule add https://github.com/panr/hugo-theme-terminal ./themes/terminal &#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Zum Aktivieren muss ich es in der Hauptkonfiguration bekannt machen:&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-toml" data-lang="toml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /hugo.toml&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;# Add it only if you keep the theme in the `themes` directory.&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;# Remove it if you use the theme as a remote Hugo Module.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;theme&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;terminal&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Und schon baut Hugo für mich im Terminal-Theme und Originalfarben.&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/migrating-hugo-to-jekyll/hugo-initial-build-with-terminal.avif" alt="Image: One of my blog posts in original Terminal theme: Left-adjusted, filling half the screen and in dark grey, orange as contrast color, and white text." /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Dies ist nun nicht vertikal zentriert und für meinen Geschmack zu schmal ausgelegt, der Text recht klein und &amp;ldquo;meine&amp;rdquo; Blog-Farben sollen ebenfalls anders sein .&lt;/p&gt;&#10;&lt;p&gt;Daher erstelle ich eine &lt;code&gt;.css&lt;/code&gt;-Datei, die einige Eigenschaften des Basis-Themes überschreibt. Sie muss im Ordner &lt;code&gt;/static&lt;/code&gt; liegen und den Namen des Themes tragen.&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-css" data-lang="css"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;/* /static/terminal.css */&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:#a6e22e"&gt;root&lt;/span&gt;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --background: &lt;span style="color:#ae81ff"&gt;#252a34&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --foreground: &lt;span style="color:#ae81ff"&gt;#d2eaef&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --foreground-dimmed: &lt;span style="color:#ae81ff"&gt;#8b9ea2&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --accent: &lt;span style="color:#ae81ff"&gt;#25a679&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --accent-highlight: &lt;span style="color:#ae81ff"&gt;#79ae9c&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --radius: &lt;span style="color:#ae81ff"&gt;8&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;px&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --font-size: &lt;span style="color:#ae81ff"&gt;1.35&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;rem&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --line-height: &lt;span style="color:#ae81ff"&gt;1.8&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;em&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;/* Use system fonts, reduce transferred kB */&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;body&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;font-family&lt;/span&gt;: system-ui, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;apple-system, &lt;span style="color:#e6db74"&gt;&amp;#34;Segoe UI&amp;#34;&lt;/span&gt;, Roboto, Arial, &lt;span style="color:#66d9ef"&gt;sans-serif&lt;/span&gt; &lt;span style="color:#75715e"&gt;!important&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;/* make content wider */&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;body&lt;/span&gt; .&lt;span style="color:#a6e22e"&gt;container&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;max-width&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;70&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;rem&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;width&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;90&lt;/span&gt;&lt;span style="color:#66d9ef"&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Und schon sieht es fast wie früher aus.&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/migrating-hugo-to-jekyll/hugo-custom-build-with-terminal.avif" alt="Image: One of my blog posts in the modified Terminal theme: centered, filling three quarters of the screen and in dark grey, teal as contrast color, and blueish-white text with a sans-serif system font." /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h2 id="organisation-der-ordnerstruktur"&gt;Organisation der Ordnerstruktur&lt;/h2&gt;&#10;&lt;p&gt;Anders als bei der von mir verwendeten &lt;em&gt;Jekyll&lt;/em&gt;-Version verfügt &lt;em&gt;hugo&lt;/em&gt; über eingebaute Internationalisierung (&lt;code&gt;i18n&lt;/code&gt;). Dafür muss ich meine Ordnerstruktur umbauen und explizit in &lt;code&gt;/de&lt;/code&gt; und &lt;code&gt;/en&lt;/code&gt;-Ordnerinhalte sortieren. Inhalte werden nun statt auf der höheren Ordner-Ebene direkt unter &lt;code&gt;/_pages, /_announcements, /_posts&lt;/code&gt; im Ordner &lt;code&gt;content&lt;/code&gt; gehalten, was die Sache übersichtlicher macht.&lt;/p&gt;&#10;&lt;h3 id="vorher"&gt;Vorher&lt;/h3&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-sh" data-lang="sh"&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;├── _announcements&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── assets&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── audio&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── css&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── fontawesome&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── katex&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── main.scss &lt;span style="color:#75715e"&gt;# &amp;lt;-- theme import, customization&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   └── webfonts&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── docs&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── images&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── js&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── lunr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── _main.js&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── main.min.js&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── plugins&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   └── vendor&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   └── video&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── banner.js&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── CNAME&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── _config.yml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── _data&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── de&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── l10n.yml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── navigation.yml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   └── ui-text.yml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   └── en&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── l10n.yml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── navigation.yml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   └── ui-text.yml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── en&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   └── index.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── Gemfile&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── Gemfile.lock&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── _includes &lt;span style="color:#75715e"&gt;# &amp;lt;-- Layouts (HTML), partials, 3rd-party&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── index.html &lt;span style="color:#75715e"&gt;# &amp;lt;-- Landing page&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── _layouts &lt;span style="color:#75715e"&gt;# &amp;lt;-- Page layouts&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── archive.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── archive-taxonomy.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── categories.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── category.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── collection.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── compress.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── default.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── home.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── posts.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── search.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── single.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── splash.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── tag.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   └── tags.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── LICENSE&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── package.json&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── package-lock.json&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── _pages&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── 404.md &lt;span style="color:#75715e"&gt;# &amp;lt;-- German version&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;...&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;│   ├── en&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── 404.md &lt;span style="color:#75715e"&gt;# &amp;lt;-- English version&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;...&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;├── _posts/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   └── en &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── _projects/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── Rakefile&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── README.md&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── _sass&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── minimal-mistakes &lt;span style="color:#75715e"&gt;# &amp;lt;-- Styles (SCSS) for layouts and partials&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── minimal-mistakes.scss&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   └── _video.scss&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── _site/ &lt;span style="color:#75715e"&gt;# &amp;lt;-- Build artifacts&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── staticman.yml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;└── tags&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├── en&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; │   └── tags.md&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; └── tags.md&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="nachher"&gt;Nachher&lt;/h3&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-sh" data-lang="sh"&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;├── archetypes&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── assets&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   └── scss&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   └── landing.scss &lt;span style="color:#75715e"&gt;# &amp;lt;-- custom styling for my landing page&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── content&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── de&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── about.md&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── announcements/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── posts/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   └── projects/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   └── en&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── about.md&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── posts/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   └── projects/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── data&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── hugo.toml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── layouts&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── _default&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── baseof.html &lt;span style="color:#75715e"&gt;# &amp;lt;-- defines header, add-ons, content, footer structure&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── index.html &lt;span style="color:#75715e"&gt;# &amp;lt;-- landing page&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── list.html &lt;span style="color:#75715e"&gt;# &amp;lt;-- grid view&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   └── single.html &lt;span style="color:#75715e"&gt;# &amp;lt;-- post view&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── _markup&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   └── render-link.html &lt;span style="color:#75715e"&gt;# &amp;lt;-- link highlighting and function (referrer, tabs)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── _partials&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── cover.html &lt;span style="color:#75715e"&gt;# &amp;lt;-- cover (thumbnail) image rendering&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   └── math.html &lt;span style="color:#75715e"&gt;# &amp;lt;-- math rendering (katex)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   └── _shortcodes&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── audio.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── image.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   └── video.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── resources&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   └── _gen&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   └── assets/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── static&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   ├── assets&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── audio/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── css/ &lt;span style="color:#75715e"&gt;# &amp;lt;-- contains overrides for custom layouts &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── docs/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── images/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   ├── js/ &lt;span style="color:#75715e"&gt;# &amp;lt;-- math (katex) JS lives here&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   │   └── video/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   └── terminal.css &lt;span style="color:#75715e"&gt;# &amp;lt;-- Central theme override (colors, formatting, styling)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;└── themes&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; └── terminal &lt;span style="color:#75715e"&gt;# &amp;lt;-- GIT submodule: Vanilla &amp;#34;Terminal&amp;#34; theme&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Bei beiden Lösungen werden Layouts in &lt;code&gt;html&lt;/code&gt; und Stil/Format ind &lt;code&gt;CSS&lt;/code&gt; definiert. Da ich bei Jekyll mehr Plugins verwenden musste, wird das Setup größer.&lt;/p&gt;&#10;&lt;h2 id="manuelle-anpassungen-shortcodes"&gt;Manuelle Anpassungen: Shortcodes&lt;/h2&gt;&#10;&lt;p&gt;Mit &lt;em&gt;Shortcodes&lt;/em&gt; kann man in Hugo nicht-Text-Inhalte (Medien, Links, Formatierung, Stil) definieren, die dann nach definierter Vorschrift gerendert werden. Dafür legt man eine &lt;code&gt;html&lt;/code&gt;-Datei an, welche den Dekorator definiert und entsprechende &lt;code&gt;css-Klassen&lt;/code&gt; einbindet. Außerdem fügt man die gewünschte Formatierung in der überschreibenden Theme-&lt;code&gt;css&lt;/code&gt;-Datei ein.&lt;/p&gt;&#10;&lt;p&gt;Da ich hier absolut kein Profi bin, probiere ich anschließend so lange herum, bis mir das Layouting gefällt und ich keine Artefakte/Überlagerungen mehr sehe.&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-sh" data-lang="sh"&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;├── layouts&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   └── _shortcodes&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   └── my-shortcode.html &lt;span style="color:#75715e"&gt;# &amp;lt;-- Custom shortcode file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── static&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│   └── terminal.css &lt;span style="color:#75715e"&gt;# &amp;lt;-- Central theme override (colors, formatting, styling)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="hugo"&gt;Hugo&lt;/h3&gt;&#10;&lt;p&gt;Shortcodes werden in der Markdown-Datei mit folgender Syntax angezeigt:&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-md" data-lang="md"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{&amp;lt; shortcode-html-filename options &amp;gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{&amp;lt; image src=&amp;#34;/path/to/image.avif&amp;#34; alt=&amp;#34;Image: alt text&amp;#34; position=&amp;#34;right&amp;#34; &amp;gt;}}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="jekyll"&gt;Jekyll&lt;/h3&gt;&#10;&lt;p&gt;Im Vergleich dazu sehen Shortcode-ähnliche Dekoratoren in Jekyll für &lt;a href="https://jekyllrb.com/docs/liquid/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Liquid, die in Jekyll benutzte Beschreibungssprache&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; so aus:&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-md" data-lang="md"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;### Liquid in interpreter code&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{% Liquid bracket syntax %}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;### Liquid &amp;#34;shortcode&amp;#34; pendant&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{% include gallery id=&amp;#34;gallery&amp;#34; caption=&amp;#34;Eindrücke von &lt;span style="font-weight:bold"&gt;**MobFobAmp**&lt;/span&gt;. Zum Vergrößern anklicken.&amp;#34; %}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;### Liquid decorator for layouting&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{:.list-inline}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="image"&gt;image&lt;/h3&gt;&#10;&lt;p&gt;Anwendungsbeispiel zur Bildanzeige. In der nächsten Zeile folgt ein zentriertes Bild.&#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/favicon/logo_icon.avif" alt="Image: My blog logo, half a speaker chassis, half cog wheels, centered." /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&lt;/p&gt;&#10;&lt;div class="media-wrapper left"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/favicon/logo_icon.avif" alt="Image: My blog logo, half a speaker chassis, half cog wheels, left-adjusted with text flow." /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Der klassische Bild-Dekorator für Markdown &lt;code&gt;[Alt Text](/path/to/image)&lt;/code&gt; wird in Hugo standardmäßig als mittig zentriertes Bild ohne Textfluss gerendert. Für ein schönes Layouting benötige ich jedoch ein paar Varianten, welche etwas zusätzlichen Code in cascading style sheets und html erfordern.&lt;/p&gt;&#10;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/favicon/logo_icon.avif" alt="Image: My blog logo, half a speaker chassis, half cog wheels, right-adjusted with text flow." /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Bild zentriert, ohne Textfluss&lt;/li&gt;&#10;&lt;li&gt;Bild links , Textfluss rechts&lt;/li&gt;&#10;&lt;li&gt;Bild rechts, Textfluss links&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Zuerst besorgt sich Hugo Dateipfad und Bildtext. Anschließend liest es die gewünschte Formatierung aus der Markdown-Datei, welche mit der &lt;code&gt;position&lt;/code&gt;-Variable angelegt wird. Anschließend wendet sie die &lt;code&gt;image-wrapper&lt;/code&gt; Klasse aus der &lt;code&gt;.css&lt;/code&gt;-Datei darauf an. Die Ausrichtung der Bilder gilt für bessere Lesbarkeit nur für die Desktop-Variante der Webseite (&lt;code&gt;&amp;gt;768px&lt;/code&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-go-html-template" data-lang="go-html-template"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{- /* /layouts/_shortcodes/image.html */ -}}&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;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$src&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Get&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;src&amp;#34;&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;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$alt&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Get&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;alt&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;default&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&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;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$position&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Get&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;position&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;default&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;center&amp;#34;&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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;image-wrapper &lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$position&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;img&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$src&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;alt&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$alt&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; /&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&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-css" data-lang="css"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;/* /static/terminal.css */&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:#a6e22e"&gt;image-wrapper&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;center&lt;/span&gt; &lt;span style="color:#f92672"&gt;img&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:#a6e22e"&gt;image-wrapper&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;left&lt;/span&gt; &lt;span style="color:#f92672"&gt;img&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:#a6e22e"&gt;image-wrapper&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;right&lt;/span&gt; &lt;span style="color:#f92672"&gt;img&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;max-width&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;100&lt;/span&gt;&lt;span style="color:#66d9ef"&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;border&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;px&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;solid&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;var&lt;/span&gt;(&lt;span style="color:#f92672"&gt;--&lt;/span&gt;accent);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;display&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;block&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;margin-left&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;auto&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;margin-right&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;auto&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;media&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#f92672"&gt;min-width&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#f92672"&gt;768px&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:#a6e22e"&gt;image-wrapper&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;left&lt;/span&gt; &lt;span style="color:#f92672"&gt;img&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;float&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;left&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;margin-right&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;rem&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;max-width&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;50&lt;/span&gt;&lt;span style="color:#66d9ef"&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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;image-wrapper&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;right&lt;/span&gt; &lt;span style="color:#f92672"&gt;img&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;float&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;right&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;margin-left&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;rem&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;max-width&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;50&lt;/span&gt;&lt;span style="color:#66d9ef"&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;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="audio"&gt;audio&lt;/h3&gt;&#10;&lt;p&gt;Zur Darstellung des Audio-Players verwende ich die im Browser implementierten Standards.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;audio controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/audio/aa_alpha_fingered_flageolet.mp3" type="audio/mp3"&gt;&#10; Your browser does not support the audio element.&#10; &lt;/audio&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Beispielplayer: Melodie auf dem E-Bass&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&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-go-html-template" data-lang="go-html-template"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{- /* /layouts/_shortcodes/audio.html */ -}}&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;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$src&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Get&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;src&amp;#34;&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;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$caption&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Get&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;caption&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;default&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;media-wrapper&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;media-container&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;audio&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;controls&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;source&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$src&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;type&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;audio/mp3&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Your browser does not support the audio element.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;audio&lt;/span&gt;&amp;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; &lt;span style="color:#66d9ef"&gt;with&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$caption&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; &amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;media-title&amp;#34;&lt;/span&gt;&amp;gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;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; &lt;span style="color:#66d9ef"&gt;end&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; &amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In &lt;code&gt;css&lt;/code&gt; verwende ich einen Wrapper und Media-Container, um Titel im Rahmen des Players anzeigen zu können und die Formatierung zu erleichtern. Auch hier habe ich angepasstes Verhalten für ein &amp;ldquo;responsive layout&amp;rdquo; bei Mobilgeräten oder Änderungen der Größe des Browser-Fensters.&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-css" data-lang="css"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;/* /static/terminal.css */&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:#a6e22e"&gt;media-wrapper&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;width&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;100&lt;/span&gt;&lt;span style="color:#66d9ef"&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;max-width&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;100&lt;/span&gt;&lt;span style="color:#66d9ef"&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;margin&lt;/span&gt;: &lt;span style="color:#a6e22e"&gt;var&lt;/span&gt;(&lt;span style="color:#f92672"&gt;--&lt;/span&gt;line&lt;span style="color:#f92672"&gt;-&lt;/span&gt;height) &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;display&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;flex&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;justify-content&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;center&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:#a6e22e"&gt;media-container&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;width&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;100&lt;/span&gt;&lt;span style="color:#66d9ef"&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;max-width&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;768&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;px&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;display&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;block&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;border&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;px&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;solid&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;var&lt;/span&gt;(&lt;span style="color:#f92672"&gt;--&lt;/span&gt;accent);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;background-color&lt;/span&gt;: &lt;span style="color:#a6e22e"&gt;var&lt;/span&gt;(&lt;span style="color:#f92672"&gt;--&lt;/span&gt;accent);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;border-radius&lt;/span&gt;: &lt;span style="color:#a6e22e"&gt;var&lt;/span&gt;(&lt;span style="color:#f92672"&gt;--&lt;/span&gt;radius);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;overflow&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;hidden&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;text-align&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;center&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:#a6e22e"&gt;media-container&lt;/span&gt; &lt;span style="color:#f92672"&gt;audio&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:#a6e22e"&gt;media-container&lt;/span&gt; &lt;span style="color:#f92672"&gt;video&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;width&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;100&lt;/span&gt;&lt;span style="color:#66d9ef"&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;display&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;block&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:#a6e22e"&gt;media-container&lt;/span&gt; &lt;span style="color:#f92672"&gt;iframe&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;width&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;100&lt;/span&gt;&lt;span style="color:#66d9ef"&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;min-height&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;400&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;px&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;aspect-ratio&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;9&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;display&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;block&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:#a6e22e"&gt;media-title&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;word-break&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;break-word&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;hyphens&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;auto&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;color&lt;/span&gt;: &lt;span style="color:#a6e22e"&gt;var&lt;/span&gt;(&lt;span style="color:#f92672"&gt;--&lt;/span&gt;background);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;font-weight&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;bold&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;h3 id="video"&gt;video&lt;/h3&gt;&#10;&lt;p&gt;Das CSS ist identisch mit &lt;code&gt;audio&lt;/code&gt;. Es gibt nur im &lt;code&gt;html&lt;/code&gt; geringfügige Unterschiede. Der von mir verwendete Container unterstützt unter &lt;code&gt;src=&lt;/code&gt; als Quellen sowohl &amp;ldquo;embed-Links&amp;rdquo; von Webseiten wie &lt;em&gt;peertube&lt;/em&gt; als auch lokale Quelldateien.&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/2022-03-01_trolley.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;Video-Beispiel (lokal): Linearführungen mit Kugelumlauf&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&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-go-html-template" data-lang="go-html-template"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{- /* /layouts/_shortcodes/video.html */ -}}&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;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$src&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Get&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;src&amp;#34;&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;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$title&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Get&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;title&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;default&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&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;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$caption&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Get&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;caption&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;default&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;media-wrapper&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;media-container&amp;#34;&lt;/span&gt;&amp;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; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;hasPrefix&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$src&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;http&amp;#34;&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; &amp;lt;&lt;span style="color:#f92672"&gt;iframe&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$src&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;frameborder&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;0&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;allowfullscreen&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style="color:#f92672"&gt;iframe&lt;/span&gt;&amp;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; &lt;span style="color:#66d9ef"&gt;else&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; &amp;lt;&lt;span style="color:#f92672"&gt;video&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;controls&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;source&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$src&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;type&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;video/mp4&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Your browser does not support the video tag.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;video&lt;/span&gt;&amp;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; &lt;span style="color:#66d9ef"&gt;end&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;&#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; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;or&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$title&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$caption&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; &amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;media-title&amp;#34;&lt;/span&gt;&amp;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; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$title&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$title&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$caption&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&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; &amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;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; &lt;span style="color:#66d9ef"&gt;end&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; &amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="links"&gt;links&lt;/h3&gt;&#10;&lt;p&gt;Folgendes Verhalten wünsche ich mir für Links auf meinem Blog:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;interne Links sollen im selben Tab geöffnet werden und die Nutzer weiterleiten.&lt;/li&gt;&#10;&lt;li&gt;interne Links sollen zu Seiten in derselben Sprache verweisen und nicht auf die Standard-Sprache oder zurück zur Hauptseite führen.&lt;/li&gt;&#10;&lt;li&gt;externe Links sollen mit einem Symbol gekennzeichnet werden: &lt;code&gt;↗&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;externe Links sollen in einem frischen Tab geöffnet werden: &lt;code&gt;target=&amp;quot;_blank&amp;quot;&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;exterme Links sollen weder auf die ursprünglichen Inhalte zugreifen noch den Quellverweis sehen können &lt;code&gt;rel=&amp;quot;noopener noreferrer&amp;quot;&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Dafür erstelle ich unter &lt;code&gt;/layouts/_markup&lt;/code&gt; die Datei &lt;code&gt;render-link.html&lt;/code&gt;.&#10;Den Code hierfür und seine Hintergründe stelle ich in einem separaten Post vor: &lt;a href="https://blog.schallbert.de/hugo-create-language-specific-permalinks/"&gt;Hugo Multi-language: Interne Permalinks&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;Der Override für CSS sieht wie folgt aus.&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-css" data-lang="css"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;/* /static/terminal.css */&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:#a6e22e"&gt;external-link&lt;/span&gt; .&lt;span style="color:#a6e22e"&gt;external-link-icon&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;display&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;inline-block&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;font-size&lt;/span&gt;: &lt;span style="color:#a6e22e"&gt;var&lt;/span&gt;(&lt;span style="color:#f92672"&gt;--&lt;/span&gt;font&lt;span style="color:#f92672"&gt;-&lt;/span&gt;size);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;vertical-align&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;middle&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;transform&lt;/span&gt;: translateY(&lt;span style="color:#ae81ff"&gt;-0.1&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;em&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;h2 id="manuelle-anpassungen-partials"&gt;Manuelle Anpassungen: Partials&lt;/h2&gt;&#10;&lt;p&gt;Partials kann man sich vorstellen wie die Layout-Bausteine für eine Hugo-Website. Sie werden unter &lt;code&gt;/layouts/_partials&lt;/code&gt; abgelegt und überschreiben die Standards des verwendeten Themes. Dabei wird die Ursprungsdatei gar nicht mehr geladen, was ein Herüberkopieren der Originaldatei in den Ordner mit anschließender manueller Anpassung empfehlenswert macht.&lt;/p&gt;&#10;&lt;h3 id="index"&gt;index&lt;/h3&gt;&#10;&lt;p&gt;Die Startseite für den Blog ist komplett selbt geschrieben und liegt nicht in Markdown, sondern in &lt;code&gt;html&lt;/code&gt; vor. Sie zeigt ihre Inhalte anders an als alle anderen Blog-Seiten und daher lohnte es sich für mich nicht, Templates zu programmieren. In der entsprechenden &lt;code&gt;/layouts/_partials/index.html&lt;/code&gt; lade ich dafür notwendige &lt;code&gt;scss&lt;/code&gt; und &lt;code&gt;css&lt;/code&gt;-Dateien und gebe die Struktur der Seite vor. Die folgende Tabelle gibt einen Überblick über meine Änderungen.&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;File&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Purpose&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Customization&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;cover&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;thumbnail / cover image for overview pages, lists, and grids&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Add custom cover image decorator, alt text&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;head&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Defines Favicon, search console, page parameters, feeds etc.&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Custom favicon path&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;header&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Page header containing menu, logo, navigation etc.&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Add logo and subtitle to menu, lang selector&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;logo&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Custom page logo, title, subtitle&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Full&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;math&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Displays math content with Latex/Katex&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;None, implement math support&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="math"&gt;math&lt;/h3&gt;&#10;&lt;p&gt;Mein Blog soll mathematische Notation unterstützen. Dafür benutze ich &lt;a href="https://katex.org/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Katex&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&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-go-html-template" data-lang="go-html-template"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{- /* /layouts/_partials/math.html */ -}}&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;&amp;lt;&lt;span style="color:#f92672"&gt;link&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;rel&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;stylesheet&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;href&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;/assets/css/katex.min.css&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;script&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;defer&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;/assets/js/katex.min.js&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style="color:#f92672"&gt;script&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;script&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;defer&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;/assets/js/contrib/auto-render.min.js&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;onload&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;renderMathInElement(document.body);&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style="color:#f92672"&gt;script&lt;/span&gt;&amp;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;&amp;lt;&lt;span style="color:#f92672"&gt;script&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; document.&lt;span style="color:#a6e22e"&gt;addEventListener&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;DOMContentLoaded&amp;#34;&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;function&lt;/span&gt;() {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;renderMathInElement&lt;/span&gt;(document.&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;, {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;delimiters&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:#a6e22e"&gt;left&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;\\[&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;right&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;\\]&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;display&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;}, &lt;span style="color:#75715e"&gt;// block&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#a6e22e"&gt;left&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;$$&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;right&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;$$&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;display&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;}, &lt;span style="color:#75715e"&gt;// block&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#a6e22e"&gt;left&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;\\(&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;right&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;\\)&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;display&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;}, &lt;span style="color:#75715e"&gt;// inline&#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:#a6e22e"&gt;throwOnError&lt;/span&gt; &lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&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;&amp;lt;/&lt;span style="color:#f92672"&gt;script&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="cover-image"&gt;cover image&lt;/h3&gt;&#10;&lt;p&gt;Unter der Bezeichnung &lt;code&gt;cover&lt;/code&gt; verstehe ich ein Titelbild eines Blog-Posts. Es wird in Übersichtsseiten, Artikel- und Tag-Listen etc. angezeigt und soll die Inhalte des Blog-posts unterstützend beschreiben. Auch mein &lt;code&gt;RSS&lt;/code&gt;-Feed soll diese Bilder für die Übersicht verschiedener Artikel auf Endgeräten meiner Leserschaft verwenden.&lt;/p&gt;&#10;&lt;p&gt;Es ist besonders stark komprimiert und benötigt wenig Übertragungsbandbreite. Daher ist seine Auflösung gering (&lt;code&gt;440x220&lt;/code&gt; oder &lt;code&gt;640x352&lt;/code&gt;). Für einen einheitlichen Look sind sie zudem stets monochrom.&lt;/p&gt;&#10;&lt;p&gt;Die Markdown-Syntax zum Einbinden des Covers sehe ich wie folgt vor:&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-yml" data-lang="yml"&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:#f92672"&gt;cover_alt&lt;/span&gt;: &lt;span style="color:#f92672"&gt;&amp;#39;Image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Hugo console build output. It shows a full build time below 2sec. (~90% reduction over build time with Jekyll)&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:#f92672"&gt;cover&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;/assets/images/migrating-hugo-to-jekyll/hugo-jekyll-build-compare-cover.avif&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;Anschließend erstelle ich unter &lt;code&gt;layouts/_partials&lt;/code&gt; die Datei &lt;code&gt;cover.html&lt;/code&gt; und schreibe folgenden Code hinein:&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-go-html-template" data-lang="go-html-template"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{- /* /layouts/_partials/cover.html */ -}}&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;{{- /* Handles cover-image generation */ -}}&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;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$cover&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&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;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$autoCover&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;default&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$.Site.Params.autoCover&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&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;&#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; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;index&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Params&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;cover&amp;#34;&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;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Resources.GetMatch&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Params.Cover&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;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$cover&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.Resources.GetMatch&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Params.Cover&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.RelPermalink&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;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;else&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;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$cover&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;absURL&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Params.Cover&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;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&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;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$.Site.Params.AutoCover&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;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;not&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Params.Cover&lt;/span&gt;&lt;span style="color:#f92672"&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;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Resources.GetMatch&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;cover.*&amp;#34;&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;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$cover&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.Resources.GetMatch&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;cover.*&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.RelPermalink&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;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&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;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&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;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&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;&#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;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$cover&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;&amp;lt;!-- Cover image found --&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;img&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$cover&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&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:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;post-cover&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:#a6e22e"&gt;alt&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Params.cover_alt&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;plainify&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;default&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.Description&lt;/span&gt;&lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;plainify&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&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:#a6e22e"&gt;title&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Params.CoverCredit&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;plainify&lt;/span&gt;&lt;span style="color:#f92672"&gt;|&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;default&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.Title&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;plainify&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; /&amp;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; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Der Code sucht und extrahiert den Pfad zum Coverbild. Sollte ein valider Pfad gefunden werden, wird es als &lt;code&gt;post-cover&lt;/code&gt; mit entsprechendem CSS angezeigt.&lt;/p&gt;&#10;&lt;h2 id="manuelle-anpassungen-layouts"&gt;Manuelle Anpassungen: Layouts&lt;/h2&gt;&#10;&lt;p&gt;Damit sich meine Seite in Hugo weiterhin ganz ähnlich anfüllt wie zuvor in Jekyll muss ich in den Layouts ein paar Änderungen vornehmen. Im Folgenden habe ich sie beschrieben.&lt;/p&gt;&#10;&lt;h3 id="baseofhtml"&gt;baseof.html&lt;/h3&gt;&#10;&lt;p&gt;Hier werden die Basis-Inhalte definiert. Definitionen für Kopf- und Fußzeile werden importiert und Stylesheets eingebunden. Die allermeisten oben beschriebenen Änderungen werden an dieser Stelle importiert.&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-go-html-template" data-lang="go-html-template"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{- /* /layouts/baseof.html */ -}}&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;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;html&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;lang&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$.Site.Language&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;head&lt;/span&gt;&amp;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; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Param&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;math&amp;#34;&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;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;partialCached&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;math.html&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&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;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&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;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;block&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;title&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&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; &amp;lt;&lt;span style="color:#f92672"&gt;title&lt;/span&gt;&amp;gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.IsHome&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$.Site.Title&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Title&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt; :: &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$.Site.Title&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;title&lt;/span&gt;&amp;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; &lt;span style="color:#66d9ef"&gt;end&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;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;partial&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;head.html&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&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; &amp;lt;&lt;span style="color:#f92672"&gt;link&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;rel&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;stylesheet&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;href&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;/assets/css/gallery.css&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;relURL&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&amp;gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;link&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;rel&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;stylesheet&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;href&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;/assets/css/media.css&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;relURL&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;head&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;body&lt;/span&gt;&amp;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; &lt;span style="color:#a6e22e"&gt;$container&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;cond&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;$.Site.Params.FullWidthTheme&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;default&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;container full&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;cond&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;$.Site.Params.CenterTheme&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;default&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;container center&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;container&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$container&lt;/span&gt; &lt;span style="color:#75715e"&gt;-}}{{-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;cond&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;$.Site.Params.oneHeadingSize&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;default&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34; headings--one-size&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&amp;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;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;partial&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;header.html&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;content&amp;#34;&lt;/span&gt;&amp;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; &lt;span style="color:#66d9ef"&gt;block&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;main&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&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;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&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; &amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;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;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;block&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;footer&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&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;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;partial&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;footer.html&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&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;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;end&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;&amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;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;&amp;lt;/&lt;span style="color:#f92672"&gt;body&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;html&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="listhtml"&gt;list.html&lt;/h3&gt;&#10;&lt;p&gt;Für Übersichtsseiten wie &lt;code&gt;Artikel&lt;/code&gt;, &lt;code&gt;Tags&lt;/code&gt;, &lt;code&gt;Projekte&lt;/code&gt; benötige ich eine Listenansicht. Dabei möchte ich je vier Artikel in einer Zeile anzeigen. Jeder Artikel bekommt ein eigenes &amp;ldquo;Kärtchen&amp;rdquo; mit Titelbild (cover), Titel, Kurzbeschreibung und - falls Verfügbar - weiteren Details wie Lesezeit. Hugos &lt;code&gt;paginator&lt;/code&gt; crawlt den entsprechenden Ordner und stellt die Inhalte zusammen, wärend mein Stylesheet &lt;code&gt;list.css&lt;/code&gt; Formatierung und Anzeige übernimmt.&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-go-html-template" data-lang="go-html-template"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;{{- /* /layouts/_default/list.html */ -}}&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;{{&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;define&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;main&amp;#34;&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; &amp;lt;&lt;span style="color:#f92672"&gt;link&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;rel&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;stylesheet&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;href&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;/assets/css/list.css&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;relURL&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&amp;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; &lt;span style="color:#66d9ef"&gt;with&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Content&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; &amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;index-content&amp;#34;&lt;/span&gt;&amp;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; &lt;span style="color:#a6e22e"&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; &amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;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; &lt;span style="color:#66d9ef"&gt;end&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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;posts posts-grid&amp;#34;&lt;/span&gt;&amp;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; &lt;span style="color:#66d9ef"&gt;range&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Paginator.Pages&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; &amp;lt;&lt;span style="color:#f92672"&gt;article&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;post on-list&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;a&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;href&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Permalink&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;post-card&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;aria-label&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Title&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&amp;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; &lt;span style="color:#a6e22e"&gt;partial&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;cover.html&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;post-card-body&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;h4&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;post-title&amp;#34;&lt;/span&gt;&amp;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; &lt;span style="color:#a6e22e"&gt;.Title&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;markdownify&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; &amp;lt;/&lt;span style="color:#f92672"&gt;h4&lt;/span&gt;&amp;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; &amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;post-excerpt&amp;#34;&lt;/span&gt;&amp;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; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Description&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; &amp;lt;&lt;span style="color:#f92672"&gt;p&lt;/span&gt;&amp;gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Description&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;p&lt;/span&gt;&amp;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; &lt;span style="color:#66d9ef"&gt;end&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; &amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;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;{{-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;and&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.Param&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;readingTime&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;eq&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;.Param&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;readingTime&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;&lt;span style="color:#f92672"&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; &amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;post-reading-time&amp;#34;&lt;/span&gt;&amp;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; &lt;span style="color:#a6e22e"&gt;.ReadingTime&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt; &lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$.Site.Params.minuteReadingTime&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;default&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;min read&amp;#34;&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; &amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;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; &lt;span style="color:#66d9ef"&gt;end&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; &amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;a&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;article&lt;/span&gt;&amp;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; &lt;span style="color:#66d9ef"&gt;end&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;&#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; &lt;span style="color:#a6e22e"&gt;partial&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;pagination.html&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&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; &amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;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; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="singlehtml"&gt;single.html&lt;/h3&gt;&#10;&lt;p&gt;Meine &lt;code&gt;single,html&lt;/code&gt; ist fast identisch mit der Standardimplementierung von &lt;a href="https://github.com/panr/hugo-theme-terminal/blob/master/layouts/_default/single.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;terminal&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Ich habe lediglich die Möglichkeit hinzugefügt, ein zum Titel gehöriges &amp;ldquo;banner-image&amp;rdquo; zu haben, und importiere die &lt;code&gt;_partial/cover.html&lt;/code&gt;-Vorschrift zum Laden eines Coverbildes.&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-go-html-template" data-lang="go-html-template"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;article&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;post&amp;#34;&lt;/span&gt;&amp;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; &lt;span style="color:#66d9ef"&gt;with&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.Params.banner&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; &amp;lt;&lt;span style="color:#f92672"&gt;img&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;.&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&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:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;post-banner&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:#a6e22e"&gt;alt&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#75715e"&gt;{{&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;$.Params.banner_alt&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;plainify&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;default&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39; &amp;#39;&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; /&amp;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; &lt;span style="color:#66d9ef"&gt;end&lt;/span&gt; &lt;span style="color:#75715e"&gt;}}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Bei all den Änderungen könnte man überlegen, ein eigenes Theme zu forken&amp;hellip; 🤔&lt;/p&gt;&#10;&lt;h2 id="direkter-vergleich"&gt;Direkter Vergleich&lt;/h2&gt;&#10;&lt;p&gt;Dann schauen wir uns jetzt mal an, wie meine Seite mit Jekyll gegenüber Hugo lädt. Hierfür öffne ich die &lt;em&gt;Entwickleroptionen (F12)&lt;/em&gt; in &lt;a href="https://www.firefox.com/de/?redirect_source=mozilla-org" target="_blank" rel="noopener noreferrer" class="external-link"&gt;meinem Browser&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, wähle das &lt;em&gt;Network&lt;/em&gt;-Tab an und klicke dort auf &lt;em&gt;All&lt;/em&gt;. Anschließend lade ich die Seite neu.&lt;/p&gt;&#10;&lt;p&gt;Dabei entsteht die folgende Auflistung:&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/migrating-hugo-to-jekyll/blog_schallbert-transfersize-loadtime-hugo.avif" alt="Image: A snapshot of my browser window with the blog&amp;#39;s landing page opened. Developer options are activated, and the lower part of the image shows a table with the individual page content files, their size, and contribution to the load time. For hugo, it transfers a total of 189kB in 175ms." /&gt;&lt;/figure&gt;&#10;&lt;/div&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/migrating-hugo-to-jekyll/blog_schallbert-transfersize-loadtime-jekyll.avif" alt="Image: A snapshot of my browser window with the blog&amp;#39;s landing page opened. Developer options are activated, and the lower part of the image shows a table with the individual page content files, their size, and contribution to the load time. For jekyll, it transfers a total of 328kB in 306ms." /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Zu erwähnen ist, dass ich hier eigentlich gar nicht Jekyll mit Hugo vergleiche, sondern lediglich deren Ausgabe und daher die gewählten Themes maßgeblich für Größe und Ladezeit verantwortlich sind. Also vergleiche ich praktisch das &lt;em&gt;minimalmistakes theme in Jekyll&lt;/em&gt; mit dem &lt;em&gt;terminal theme in Hugo&lt;/em&gt;.&lt;/p&gt;&#10;&lt;h3 id="größe"&gt;Größe&lt;/h3&gt;&#10;&lt;p&gt;Ein anderes Diagramm erhalte ich, wenn ich ganz unten links auf die Stoppuhr klicke und damit eine &lt;a href="https://firefox-source-docs.mozilla.org/devtools-user/network_monitor/performance_analysis/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;performance analysis&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ausführe.&lt;/p&gt;&#10;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/migrating-hugo-to-jekyll/blog_schallbert-landing-performance-analysis-terminaltheme.avif" alt="Image: A cake diagram as the output of firefox&amp;#39;s performance analysis tool in developer options. Analyzing the hugo&amp;#39;s terminal theme on a typical blog post of mine, images make up for three quarters of total transferred size. The last quarter is largely taken by css, and just 12kB html at a total size of 165kB." /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Die folgende Tabelle zeigt ein paar meiner Seiten im direkten Vergleich. Allerdings ist die Übersicht mit Vorsicht zu genießen: Die Werte ändern sich schon mal, wenn ich neu lade (auch mit ausgeschaltetem Cache). Gerade bei eingebundenen Inhalten Dritter wie meinen Videos kommen hier teils stark unterschiedliche Werte heraus.&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;Item&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;minimal-mistakes&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;terminal-schallbert&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Jekyll&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Hugo&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;&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;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;transferred&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;[kB]&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Diff [%]&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;time&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;[ms]&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Diff [%]&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;Landing&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;163.47&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;98.68&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;-39&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;30&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;30&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&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;Post&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2861&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1767&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;-38&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;340&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;270&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;-20&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;List page&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1507&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;140&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;-90&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;30&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;30&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;Insgesamt habe ich mit Hugo und Terminal deutliche Optimierungen vorliegen.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Manuell implementiert in Hugo hatte ich das bedingte Laden von Katex (&lt;code&gt;90kB min.css&lt;/code&gt;), Größenbeschränkungen für Medien-Vorschaudateien und insgesamt weniger CSS.&lt;/li&gt;&#10;&lt;li&gt;Automatisch in Hugo sind Medien-Vorschaudateien von den hochauflösenden Dateien abgeleitet und per Bildprozessor auf die Darstellungsverhältnisse im gebauten Zustand platzoptimiert. Diese Optimierung spart bei vielen, klein dargestellten Bildern eine Menge Bandbreite.&lt;/li&gt;&#10;&lt;li&gt;Das Terminal-Theme zeigt anders als minimalmistakes keine Kärtchen für &amp;ldquo;weitere Posts&amp;rdquo; an. Dies spart erneut die Übertragung von vier Vorschaubildern. Der Javascript-Inhalt geht außerdem gegen null - eines der Ziele, das ich mir gesteckt hatte.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="komplexität"&gt;Komplexität&lt;/h3&gt;&#10;&lt;p&gt;Ganz klar: Hugo ist hier im Vergleich zu Jekyll unschlagbar.&lt;/p&gt;&#10;&lt;p&gt;Da Themes in Hugo steckbare Module sind, benötigen sie keinen Abhängigkeitsbaum in Form von Gems wie bei Jekyll zum Nachladen und installieren. Entsprechend kann hier viel weniger kaputt gehen. Für mich verringert sich die Bauzeit für einen &amp;ldquo;full build&amp;rdquo; (zweisprachig) um &lt;strong&gt;75%&lt;/strong&gt; und auch das automatische Bauen bei Veränderung meiner Markdown-Dateien ist sowohl &lt;strong&gt;80%&lt;/strong&gt; schneller als auch aussagekräftiger: Wenn ich in Jekyll nämlich &amp;ldquo;incremental build&amp;rdquo; einschalte, dann gibt es keine aktualisieren Paginator-Seiten mehr - sprich Listenansichten und Übersichtsseiten werden nicht mit gebaut.&lt;/p&gt;&#10;&lt;p&gt;Auf der anderen Seite kann ich in Jekyll sehr flexibel und feingranular selbst eingreifen, eigene Gems entwerfen und recht einfach besondere Gimmicks für meine Seite herstellen wie ein mitscrollendes Inhaltsverzeichnis. Jenes vermisse ich in Hugo etwas.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;Für die tagtägliche Benutzbarkeit gefällt mir Hugo ganz klar besser als Jekyll.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h3 id="auslieferungcontinuous-deployment"&gt;Auslieferung/Continuous Deployment&lt;/h3&gt;&#10;&lt;p&gt;Für Jekyll gestaltete sich Bauen und Ausliefern in Gitea etwas hakelig: erst mit einem &lt;a href="https://blog.schallbert.de/gitea-action-runner-jekyll-dockerimage/"&gt;Jekyll-Dockerimage&lt;/a&gt; war ich in der Lage, &lt;a href="https://blog.schallbert.de/gitea-action-runner-native-jekyll/"&gt;ruby richtig aufzusetzen&lt;/a&gt;. Damit und weil ich meinen Actions nicht erlaube zu cachen dauert das Bauen ziemlich lang: Erst den aktuellen Docker-Container ziehen, ruby und jekyll updaten, gems installieren, die Quelldateien auschecken, anschließend bauen und ausliefern - hier vergehen gerne einmal &lt;code&gt;90 Sekunden&lt;/code&gt;, bis ein neuer Artikel live ist.&lt;/p&gt;&#10;&lt;p&gt;Für Hugo muss ich mal schauen, ob ich am Ende nicht doch einen eigenen Post mache&amp;hellip;&lt;/p&gt;&#10;</description></item><item><title>🎄 Steckbrett-Lagerfeuer</title><link>https://blog.schallbert.de/projects/fireplace/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/fireplace/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/fireplace/fireplace-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: LED-Fireplace, Arduino-controlled"&#10; title="🎄 Steckbrett-Lagerfeuer" /&gt;&#10;&lt;h2 id="projekt-steckbrief"&gt;Projekt-Steckbrief&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Schwierigkeit: Anfänger 1/5&lt;/li&gt;&#10;&lt;li&gt;Kosten: 8&amp;hellip;25€&lt;/li&gt;&#10;&lt;li&gt;Zeit ~1h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="überblick"&gt;Überblick&lt;/h2&gt;&#10;&lt;p&gt;Dies ist ein schnelles und einfaches Projekt, das zeigt, wie man mit einem Microcontrollerboard und einigen LEDs eine warme und gemütliche Wohnzimmeratmosphäre schaffen kann. An eine Powerbank angeschlossen, wird diese kleine Bastelei den ganzen Abend leuchten und eine warme und gemütliche Atmosphäre schaffen. Ich habe Teile der Idee für dieses Projekt dem Buch &amp;ldquo;Tiny AVR for the evil Genius&amp;rdquo; entliehen, in dem ich viel gelesen habe, als ich vor Jahren mit dem Entwurf eingebetteter Software in &lt;code&gt;C&lt;/code&gt; begann.&lt;/p&gt;&#10;&lt;p&gt;Die Projektkosten hängen von den gewählten Komponenten ab. Wenn ein original Arduino verwendet wird, liegen sie am oberen Ende des Bereichs. So kann man den hart arbeitenden Entwicklern und Ingenieuren von &lt;a href="https://www.arduino.cc/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Arduino&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Anerkennung zollen, die Open-Source-Lösungen für uns alle schaffen 🥳.&lt;/p&gt;&#10;&lt;h2 id="die-hardware"&gt;Die Hardware&lt;/h2&gt;&#10;&lt;p&gt;Natürlich kann man jedes Mikrocontroller-Board verwenden, das mindestens 8 digitale Ausgänge und 1 analogen Eingang unterstützt. In meinem Beispiel verwende ich ein &lt;a href="https://www.arduino.cc/hardware#nano-family" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Arduino Nano&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; (Nachbau), das ich irgendwo herumfliegen hatte. Zeit für ein Upgrade auf das &amp;ldquo;Original&amp;rdquo;&amp;hellip;&#10;Des Weiteren benötigt man ein Steckbrett, ein paar Drähte und acht LEDs. Ich empfehle, 5 warmweiße LEDs, 2 gelbe LEDs und 1 rote LED mit ähnlicher Helligkeit (Lumenzahl) zu verwenden, um Farben zu erhalten, die einem echten Feuer ähnlich sind.&lt;/p&gt;&#10;&lt;h2 id="die-software"&gt;Die Software&lt;/h2&gt;&#10;&lt;p&gt;Die Software erzeugt einen flackernden Lichteffekt mit unterschiedlicher Geschwindigkeit.&lt;/p&gt;&#10;&lt;h3 id="einrichtung"&gt;Einrichtung&lt;/h3&gt;&#10;&lt;p&gt;Beim Start definiere ich in &lt;code&gt;setup()&lt;/code&gt; die Pins, an die die LEDs angeschlossen sind, als Ausgänge.&#10;Ich lasse einen offenen Analog-Pin einmal auslesen und nehme dessen (undefinierten) Wert als Seed für den später besprochenen Pseudo-Zufallszahlengenerator.&lt;/p&gt;&#10;&lt;h3 id="schleife"&gt;Schleife&lt;/h3&gt;&#10;&lt;p&gt;In der Endlosschleife berechnet das Programm eine neue Zufallszahl und übergibt diese dann an die nachgeschalteten Funktionen. Hier erzeugt ein Zufallszahlengenerator einen Wert, der dann verwendet wird, um&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;zu bestimmen, welche der acht LEDs leuchten soll&lt;/li&gt;&#10;&lt;li&gt;die Zeit festzulegen, für die dieser LED-Status beibehalten werden soll.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Schließlich werden die LEDs entsprechend gesetzt und die Schleife wiederholt sich am Anfang.&lt;/p&gt;&#10;&lt;h3 id="linear-rückgekoppeltes-schieberegister"&gt;Linear rückgekoppeltes Schieberegister&lt;/h3&gt;&#10;&lt;p&gt;Der Zufallszahlengenerator ist als sogenanntes &lt;a href="https://en.wikipedia.org/wiki/Linear-feedback_shift_register" target="_blank" rel="noopener noreferrer" class="external-link"&gt;linear feedback shift register (LSFR)&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; mit einer Länge von 32bit implementiert (wiederholt sein Muster nach max. 2^32 Schritten). Diese Implementierung verwendet den &amp;ldquo;Galois-Typ&amp;rdquo; von LFSRs.&lt;/p&gt;&#10;&lt;h3 id="galois-lfsr-implementierung"&gt;Galois LFSR Implementierung&lt;/h3&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:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;loop&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; iRandNum &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (iRandNum &lt;span style="color:#f92672"&gt;&amp;gt;&amp;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:#f92672"&gt;-&lt;/span&gt;(iRandNum &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1u&lt;/span&gt;) &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0xd0000001u&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In dieser Codezeile passiert eine Menge, also schauen wir sie uns mal genauer an.&lt;/p&gt;&#10;&lt;p&gt;Wesentlich für diese Art von Zufallszahlengeneratoren ist die &lt;a href="https://en.wikipedia.org/wiki/XOR_gate" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;code&gt;XOR&lt;/code&gt;-Operation&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, die durch ein &lt;code&gt;^&lt;/code&gt; in der Mitte der Codezeile angezeigt wird. XOR&amp;quot; bedeutet &amp;ldquo;eXclusive OR&amp;rdquo;, das nur dann eine logische 1 ausgibt, wenn die logischen Eingänge A und B &lt;em&gt;unterschiedlich&lt;/em&gt; sind. Wenn &amp;ldquo;A&amp;rdquo; und &amp;ldquo;B&amp;rdquo; beide &amp;ldquo;0&amp;rdquo; oder beide &amp;ldquo;1&amp;rdquo; sind, wird dieses Gatter &amp;ldquo;0&amp;rdquo; ausgeben.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Beispiel für binäres XOR:&lt;/strong&gt;&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt; A B Ergebnis&#10;0b00001111 ^ 0b00110011 = 00111100&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Links vom &lt;code&gt;XOR&lt;/code&gt;-Operator wird die aktuelle Zufallszahl um &lt;code&gt;1&lt;/code&gt; nach rechts verschoben (&lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;). Dabei wird ihr gesamter Inhalt wird um ein Bit geschoben. In numerischer Hinsicht bedeutet dies, dass der Wert ganzzahlig durch zwei dividiert wird.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Beispiel für binäres SHIFT:&lt;/strong&gt;&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt; A B Ergebnis Als Zahl:&#10;0b00001111 &amp;gt;&amp;gt; 1 = 0b00000111 15 &amp;gt;&amp;gt; 1 = 7&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;OK, analysieren wir die obige Zeile Code weiter.&#10;Direkt neben dem &lt;code&gt;XOR&lt;/code&gt;-Operator findet eine weitere Operation statt: Ein logisches &lt;code&gt;AND&lt;/code&gt; (&lt;code&gt;&amp;amp;&lt;/code&gt;) zwischen unserer aktuellen Zufallszahl und &lt;code&gt;1u&lt;/code&gt;, was &amp;ldquo;unsigned 1&amp;rdquo; bedeutet. Dabei werden alle höheren Bits des Zufallszahlengenerators maskiert. Das Ergebnis dieser Aktion ist eine einfache &amp;ldquo;0&amp;rdquo;, wenn die Zahl eine &amp;ldquo;0&amp;rdquo; als niedrigstwertiges Bit hat usw.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Beispiel für binäres UND:&lt;/strong&gt;&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt; A B Ergebnis &#10;0b00001111 &amp;amp; 1 = 0b00000001&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Die Negation am Anfang dieses Terms bedeutet, dass, wenn das Ergebnis des vorherigen &lt;code&gt;&amp;amp;&lt;/code&gt; &lt;code&gt;0&lt;/code&gt; war, der gesamte Term wieder &lt;code&gt;0b00000000...&lt;/code&gt; sein wird. Wenn das Ergebnis &lt;code&gt;-1&lt;/code&gt; lautet, sieht der 2er-Komplement-Binärcode stattdessen wie folgt aus: &lt;code&gt;0b1111111...&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Dies ist eine einfache Möglichkeit, einen einzelnen booleschen Wert auf einen beliebigen längeren Datentyp anzuwenden, ohne eine Verzweigungslogik wie bei einer &lt;code&gt;if()&lt;/code&gt;-Klausel zu verwenden.&lt;/p&gt;&#10;&lt;p&gt;Und schließlich gibt es ein weiteres logisches &lt;code&gt;AND&lt;/code&gt; zwischen diesem Begriff und einer festen Bitmaske &lt;code&gt;0xd0000001u&lt;/code&gt;. Letztere sind die &amp;ldquo;Anzapfungen&amp;rdquo; des LFSR, d.h. welche Bits des aktuellen Wertes in das System zurückgeführt werden. Die Aufschlüsselung dieses Hex-Wertes in Boolesche Werte zeigt: 0xd = 0b1101&amp;quot;, d.h. die Bits 32, 31, 29 und 1 werden mit der aktuellen Zufallszahl maskiert, so dass nur ihre Werte unberührt bleiben, während alle anderen auf &amp;ldquo;0&amp;rdquo; gesetzt werden.&lt;/p&gt;&#10;&lt;h3 id="galois-lfsr-zusammenfassung"&gt;Galois LFSR Zusammenfassung&lt;/h3&gt;&#10;&lt;p&gt;Puh, das war eine Menge. Es ist Zeit für ein paar Minuten Pause.&lt;/p&gt;&#10;&lt;p&gt;Gut, fassen wir die Codezeile oben nochmal zusammen:&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;iRandNum &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (iRandNum &lt;span style="color:#f92672"&gt;&amp;gt;&amp;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:#f92672"&gt;-&lt;/span&gt;(iRandNum &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1u&lt;/span&gt;) &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0xd0000001u&lt;/span&gt;);&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;Bedeutung: &amp;ldquo;Wenn das niedrigstwertige Bit der aktuellen Zufallszahl Null ist, dann verschiebe die Zufallszahl einfach nach rechts. Andernfalls XOR anwenden mit den abgegriffenen Bits der aktuellen Zufallszahl.&amp;rdquo;&lt;/em&gt;&lt;/p&gt;&#10;&lt;p&gt;LFSRs sind weit verbreitet in der &lt;a href="https://courses.cs.washington.edu/courses/cse466/12au/calendar/06-Modulation-posted.pdf" target="_blank" rel="noopener noreferrer" class="external-link"&gt;drahtlosen Kommunikationstechnik&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, sie sind interessant für &lt;a href="https://stackoverflow.com/questions/25415724/understanding-two-different-ways-of-implementing-crc-generation-with-lfsr" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Prüfsummenberechnungen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, [Kryptographen](https://medium. com/@czapfel/an-introduction-to-lfsrs-for-cryptography-bf2602640e91) und können sogar bei &lt;a href="https://evoniuk.github.io/posts/pitfall.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Programmierspielen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; nützlich sein, z.B. bei der Erstellung prozeduraler Karten. LFSRs sind einfach zu implementieren (wie man oben sieht, benötigen sie im Extremfall sogar nur eine einzige Codezeile!) und verbrauchen nicht viele Ressourcen. Hier ist ein hervorragender &lt;a href="https://datagenetics.com/blog/november12017/index.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Blogpost&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, der sich eingehend mit LFSRs beschäftigt, wenn Sie eher der visuelle Typ sind (und weniger der logische).&#10;Nachteilig ist, dass ihre &amp;ldquo;Zufallsqualität&amp;rdquo; schlecht ist, weil sie deterministisch sind.&lt;/p&gt;&#10;&lt;p&gt;Alles in allem fand ich das Thema so interessant, dass ich mich entschlossen habe, hier einen LFSR manuell zu implementieren, anstatt die Arduino-Funktion &lt;code&gt;random()&lt;/code&gt; zu verwenden.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://www.c64-wiki.de/images/9/9d/Pitfall_Animation2.gif" alt="Gif: Pitfall! Game scene" /&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Pitfall! Spielszene: prozedurales Leveldesign mittels LFSR&lt;/span&gt;&lt;a href="https://www.c64-wiki.de/wiki/Datei:Pitfall_Animation2.gif#file" class="attr-link" aria-label="Attribution 1"&gt;&#10; &lt;sup class="attr-id"&gt;[1]&lt;/sup&gt;&#10; &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h3 id="ein-problem-bleibt"&gt;Ein Problem bleibt&lt;/h3&gt;&#10;&lt;p&gt;Nun, wenn Sie die Codezeile vollständig verstanden haben, sehen Sie, was passiert, wenn die Zufallszahl nur Nullen enthält.&lt;/p&gt;&#10;&lt;p&gt;Er wird nie wieder aus diesem Zustand herauskommen 💀.&lt;/p&gt;&#10;&lt;p&gt;Deshalb sollte man diese Art von LFSR niemals mit &amp;ldquo;0&amp;rdquo; initialisieren.&lt;/p&gt;&#10;&lt;h3 id="ansteuerung-der-leds"&gt;Ansteuerung der LEDs&lt;/h3&gt;&#10;&lt;p&gt;Der Rest der Software hat viel mehr Zeilen, macht aber auch viel mehr langweilige Sachen. Sie nimmt den generierten 32-Bit-Zufallswert und lässt ihn an den 8 LED-Ausgängen vorbeilaufen, d.h. jede LED &amp;ldquo;sieht&amp;rdquo; jedes Bit des Wertes. Zwischen jeder dieser Iterationen wird eine zufällige Verzögerung verwendet, so dass die Flackergeschwindigkeit auf eine Änderungsrate begrenzt ist, die wir Menschen noch wahrnehmen können.&lt;/p&gt;&#10;&lt;h2 id="die-hardware-1"&gt;Die Hardware&lt;/h2&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/fireplace/fireplace_detail.jpg" alt="Image: fireplace breadboard design detail view" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Zum Glück ist die Hardware einfacher zu erklären als die Software:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Ein Arduino Nano-Board ist mit einem Prototyping-Board verbunden.&lt;/li&gt;&#10;&lt;li&gt;Anoden der acht LEDs &lt;code&gt;(+)&lt;/code&gt; sind mit den digitalen Ausgängen &lt;code&gt;D2...D9&lt;/code&gt; verbunden.&lt;/li&gt;&#10;&lt;li&gt;Ihre Kathoden werden mit Jumpern gebrückt und mit dem &amp;ldquo;GND&amp;rdquo; des Arduino verbunden.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Dies ist die einfachste Konstruktion, die mir für diese Aufgabe einfiel.&lt;/p&gt;&#10;&lt;h2 id="verbesserungen"&gt;Verbesserungen&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Jede LED sollte einen Vorwiderstand bekommen.&#10;&lt;ul&gt;&#10;&lt;li&gt;In meinem Design ist der Strom nur durch den maximalen Strom begrenzt, den der Arduino Pin treiben kann.&lt;/li&gt;&#10;&lt;li&gt;Entweder kann die LED deutlich mehr als &lt;code&gt;40mA&lt;/code&gt; Spitzenstrom verkraften oder sie wird unweigerlich irgendwann durchbrennen.&lt;/li&gt;&#10;&lt;li&gt;Bei der Berechnung des Widerstands sollte man sich an das Datenblatt der LED halten, da verschiedene LED-Farben unterschiedliche Spannungen haben.&lt;/li&gt;&#10;&lt;li&gt;Beispiel für eine Widerstandsberechnung bei einer Versorgungsspannung von &lt;code&gt;5V&lt;/code&gt; für eine Warmweiße LED: &lt;code&gt;20mA @ 3.1V --&amp;gt; R = U/I = (Usup-Uled) / I = 1.9 / 0.02 = 95Ohm&lt;/code&gt;. Nächster Widerstand aus der E6-Reihe &lt;code&gt;100Ohm&lt;/code&gt;.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;Jede LED könnte einen Parallelkondensator erhalten.&#10;&lt;ul&gt;&#10;&lt;li&gt;Dies würde den Flackereffekt glätten.&lt;/li&gt;&#10;&lt;li&gt;Man könnte Videos aufnehmen, in denen die Kamera nicht durch das Flackern der LEDs gestört wird. Das wäre doch schön.&lt;/li&gt;&#10;&lt;li&gt;Beispiel Kondensatorberechnung: Durchschnittlicher Strom im ausgeschalteten Zustand &lt;code&gt;20mA&lt;/code&gt;, erlaubter Spannungsabfall &lt;code&gt;0.6V&lt;/code&gt; innerhalb von &lt;code&gt;1ms&lt;/code&gt;. Vorgeschlagene Kapazität ist dann &lt;code&gt;20mA * 1ms / 0.6V = 33uF&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="zeit-für-eine-diy-replik"&gt;Zeit für eine DIY-Replik?&lt;/h2&gt;&#10;&lt;p&gt;Bitteschön!&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://blog.schallbert.de/led-fireplace-diy/"&gt;Steckbrett-Lagerfeuer DIY Anleitung&lt;/a&gt;&lt;/p&gt;&#10;</description></item><item><title>1 Jahr CNC 🎂</title><link>https://blog.schallbert.de/projects/one-year-zerspanobert/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/one-year-zerspanobert/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/machine-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Zerspanobert CNC machine"&#10; title="1 Jahr CNC 🎂" /&gt;&#10;&lt;h2 id="ein-jahr--meine-portalfräsmaschine"&gt;Ein Jahr &lt;a href="https://blog.schallbert.de/portal-milling-build/"&gt;&amp;lsquo;Zerspanobert&amp;rsquo;&lt;/a&gt;, meine Portalfräsmaschine&lt;/h2&gt;&#10;&lt;p&gt;Wir schreiben März 2023. Meine CNC-Maschine ist seit einem Jahr in Betrieb. Es war ein gutes Jahr. Ich habe eine Menge gelernt und hatte viel Spaß beim Schreiben von Artikeln über meine Fortschritte. Natürlich konnte ich nicht jedes Projekt das ich in dieser Zeit durchgeführt habe dokumentieren bloggen, dennoch soll dieser Artikel das Jahr ein wenig Revue passieren lassen&lt;/p&gt;&#10;&lt;h3 id="die-maschine"&gt;Die Maschine&lt;/h3&gt;&#10;&lt;p&gt;Ich kann inzwischen sagen, dass ich die richtige Maschine für meinen Bedarf ausgewählt habe. Es handelt sich um die zweitgünstigste Maschine von Sorotec. Sie bietet viele Eigenschaften &amp;ldquo;gehobenerer&amp;rdquo; Portalfräsmaschinen, wie z.B. Kugelumlaufspindeln, &lt;code&gt;3Nm&lt;/code&gt; Stepper, &lt;code&gt;48V&lt;/code&gt; Kleinspannungsschiene, vorgespannte Linearführungen. Um den Preis moderat zu halten, wurde allerdings auf andere Dinge verzichtet. So besitzt sie weder Riemenantriebe für die Schrittmotoren um Platz zu sparen noch doppelte Linearführungen an der Z-Achse oder zusätzliche Versteifungen für Portal, Z-Achse und Hauptrahmen.&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/1year_cnc/z_axis.jpg" alt="Bild: Detailansicht Z-Achse" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Deshalb muss man wissen, welche Lasten grenzwertig sind und sich etwas zurückhalten. Aber das gilt wahrscheinlich auch für steifere Maschinen - sie heben nur die Grenzen an. Was bei meiner Einstellung schnellere Vorschübe oder höhere Schnitttiefen verhindert, ist der Oberfräsenmotor und die Art und Weise, wie er an der Z-Achse befestigt ist.&lt;/p&gt;&#10;&lt;p&gt;Der Motor ist eindeutig überfordert mit&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Birke Multiplex, Vollnutenfräsen, 6mm Fräser 2-schneidig, &lt;code&gt;S24000&lt;/code&gt;, &lt;code&gt;Z+12mm&lt;/code&gt;, &lt;code&gt;F4000&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;HPL, Vollnutenfräsen, 6mm Fräser 2-Schneide, &lt;code&gt;S24000&lt;/code&gt;, &lt;code&gt;Z+6.5mm&lt;/code&gt;, &lt;code&gt;F3500&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Aluminium AlMg1, Vollnutenfräsen, 6mm Fräser einschneidig, &lt;code&gt;S24000&lt;/code&gt;, &lt;code&gt;Z+3mm&lt;/code&gt;, &lt;code&gt;F1600&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Wenn jedoch Schnitttiefe und/oder den Vorschub reduziert wird, kann die Maschine die Arbeit sehr gut ausführen.&#10;Die Genauigkeit der Maschine ist ziemlich gut; die wirklich begrenzenden Faktoren sind hier die manuelle Bestimmung des Werkstücknullpunkts sowie der Werkzeuglängensensor aber nicht die Maschine selbst.&lt;/p&gt;&#10;&lt;p&gt;Der Vakuumtisch eröffnete die Welt der feinen Gravuren mit Graviersticheln und V-Fräsern. Das Problem war vorher, dass die Verformung des Werkstücks und Ungenauigkeiten des Arbeitstisches dazu führten, dass die Gravuren von sehr unterschiedlicher Qualität waren und größere Toleranzen in der Breite aufwiesen, als ich erwartet hatte. Diese Probleme gibt es jetzt nicht mehr, ebenso wenig wie die zusätzliche Arbeit, Vorrichtungen für kleine oder sperrige Werkstücke herzustellen. Das Halten und Spannen wurde um ein Vielfaches einfacher. Außerdem konnte ich die Z-Sicherheitshöhe reduzieren, was Fräsarbeiten um bis zu 20 % beschleunigte.&lt;/p&gt;&#10;&lt;p&gt;Ich konzentrierte mich darauf, die Bearbeitungszeit so weit wie möglich zu reduzieren, damit ich nicht mehr Zeit als nötig mit der Überwachung der Maschine in der kalten Werkstatt verbringen musste. Deshalb habe ich die Maschine und die Fräser absichtlich an ihre Grenzen gebracht was Eilganggeschwindigkeit, Vorschub und Schnitttiefe angeht. Als ich diese Grenzen herausgefunden hatte, drosselte ich die Parameter um 20-30 %, um auf Nummer sicher zu gehen und die Wiederholgenauigkeit hoch zu halten.&lt;/p&gt;&#10;&lt;h3 id="upgrades"&gt;Upgrades&lt;/h3&gt;&#10;&lt;p&gt;Ich habe die Maschine mit einem &lt;a href="https://blog.schallbert.de/why-vacuum-table/"&gt;Vakuumtisch&lt;/a&gt; aufgerüstet und konnte viel über Haltekräfte und Volumenstrom lernen, was zü meinem Artikel zur &lt;a href="https://blog.schallbert.de/cnc-vacuum-pumps/"&gt;Auswahl der geeigneten Vakuumlösung&lt;/a&gt; führte. Ich habe Schwerschaummatten zur Vibrations- und Schalldämmung installiert. Man kann sie im Bild unten etwas schlecht erkennen, aber der gesamte Arbeittisch ist mit Dämmmatten unterfüttert.&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/1year_cnc/accessories.jpg" alt="Image: Accessories and machine controller" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Außerdem habe ich eine Absauganlage mit Zyklonabscheider installiert, die während des Fräsvorganges läuft. Die Menge an Spänen, die die Fräse produziert ist wirklich erstaunlich und so bin ich froh, dass ich die Anlage bereits seit Beginn meiner Arbeiten zur Verfügung habe.&lt;/p&gt;&#10;&lt;p&gt;Bald werde ich eine HF-Spindel statt meinem Oberfräsmotor einbauen und so Vibrationen, Toleranzen und Geräuschpegel weiter verringern. Letztendlich sollte ich damit die Vorschubwerte erreichen, die ich oben angegeben habe.&lt;/p&gt;&#10;&lt;h3 id="materialien"&gt;Materialien&lt;/h3&gt;&#10;&lt;p&gt;Ich habe mit Hartholz, &lt;a href="https://blog.schallbert.de/negative-carving-with-estlcam/#engrave"&gt;Multiplex&lt;/a&gt;, &lt;a href="https://blog.schallbert.de/projects/cnc_spoilboard/"&gt;MDF&lt;/a&gt;, Acrylglas (PMMA), &lt;a href="https://hobbyline.info/forum/index.php?thread/717-hobbyglas-polystyrol-fr%C3%A4sen/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Polystyrol&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://blog.schallbert.de/cnc-router-overload/"&gt;Hochdrucklaminaten (HPL)&lt;/a&gt; und &lt;a href="https://blog.schallbert.de/cut-dibond/"&gt;Aluminium-Komposit&lt;/a&gt; gearbeitet.&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/1year_cnc/materials.jpg" alt="Image: Material samples that I worked with" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Bei all diesen Werkstoffen musste ich lernen, wie sich die Materialien verhalten und mit welchen Parametern die Fräser betrieben werden müssen, um qualitativ hochwertige Ergebnisse zu erzielen. Einige Materialien waren so anspruchsvoll, dass meine Maschine überlastet war, bei anderen hatte ich &lt;a href="https://blog.schallbert.de/cnc-vibrates/"&gt;Vibrationsprobleme&lt;/a&gt; oder Probleme, &lt;a href="https://blog.schallbert.de/cut-dibond-tests/"&gt;die gewünschte Qualität zu erreichen&lt;/a&gt;. Aber ich konnte immer aus Fehlschlägen lernen und meine Abläufe verbessern, um sie in Zukunft 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/1year_cnc/pmma.jpg" alt="Bild: Acryl-Absaugschlauchhalter" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Das Bild oben zeigt eine Absaugschlauchklemme aus PMMA (Acrylglas), die ich an der Z-Achse meiner CNC angebracht habe. Ich habe recht lange dafür gebraucht, die minimalen Fasen genau richtig hinzubekommen und Schlichtvorgänge angewendet, wo ich glaubte, dass sie eine bessere Oberflächengüte bieten könnten.&lt;/p&gt;&#10;&lt;h3 id="software"&gt;Software&lt;/h3&gt;&#10;&lt;p&gt;&lt;a href="https://blog.schallbert.de/assets/images/qr-codengrave/app-thumb.jpg"&gt;Bild: Hauptbildschirm der Qr-codengrave-App&lt;/a&gt;{: .align-right}&#10;Ich habe die &lt;a href="https://blog.schallbert.de/macros-for-cnc/"&gt;Makros&lt;/a&gt; der Maschine sowie &lt;a href="https://blog.schallbert.de/portal-milling-setup/"&gt;die Kinematikparameter&lt;/a&gt; optimiert, und sogar eine vollständige Anwendung, mit der die CNC &lt;a href="https://blog.schallbert.de/projects/qr-codengrave/"&gt;QR-Codes&lt;/a&gt; in praktisch jedes Plattenmaterial gravieren kann, programmiert. Diese QR-Codes werden von Handykameras bestens erkannt.&lt;/p&gt;&#10;&lt;p&gt;Als meine Gravurprojekte immer komplexer wurden, stürzte mein CAM-System bei der Berechnung der Pfade für die Bearbeitung mehrerer Exemplare ständig ab. Daher musste ich die Aneinanderreihung manuell durchführen, was mich dazu brachte, mich mit Koordinatenverschiebungsbefehlen wie &lt;code&gt;G54...G59.3&lt;/code&gt;, &lt;code&gt;G68&lt;/code&gt; und &lt;code&gt;G92&lt;/code&gt; zu beschäftigen, die, wenn sie nicht richtig verstanden werden, die Werkstücke arg in Mitleidenschaft ziehen.&#10;Dies half mir, die numerische Steuerung und ihre Interpretersprache besser zu verstehen.&lt;/p&gt;&#10;&lt;p&gt;Nicht zuletzt war die Lernkurve, die ich durch die Arbeit mit &lt;a href="https://blog.schallbert.de/freecad-get-started/"&gt;FreeCAD&lt;/a&gt; und &lt;a href="https://blog.schallbert.de/negative-carving-with-estlcam/"&gt;Estlcam&lt;/a&gt; durchlaufen musste, ziemlich steil. Aber da musste ich durch, sonst hätte ich meine Teile nicht fertigen können.&lt;/p&gt;&#10;&lt;h3 id="hilfe-und-unterstützung"&gt;Hilfe und Unterstützung&lt;/h3&gt;&#10;&lt;p&gt;Zum Glück hatte ich immer Leute, die mir helfen wollten und konnten. Meine Frau war immer für mich da und erlaubte mir, mir Zeit dafür zu nehmen, und meine Kinder besuchten mich häufig im Keller, so dass ich nie einsam war. Mir meiner Frau und einigen guten Freunden konnte ich immer reden, mir Rat geben lassen, wenn die Arbeit frustrierend wurde. Aber auch von kommerzieller Seite hatte ich hervorragende Unterstützung:&lt;/p&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper left"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/sorotec.jpg" alt="Bild: Sorotec Logo" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Vielen Dank an Willy, Roy und Alex von &lt;a href="https://www.sorotec.de/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Sorotec&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; für Ratschläge, Tipps und Tricks, die mir geholfen haben, meine Maschine zu handhaben und eine bessere Lebensdauer aus den Fräsern herauszuholen. Ich habe den direkten Telefonsupport und die schnellen Bearbeitungszeiten für Fräserlieferungen und Garantiereparaturen geschätzt. Ich besuche auch gerne das &lt;a href="https://hobbyline.info/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;hobbyline CNC forum&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; für Diskussionen und um meine Geschichten dort zu veröffentlichen und zu diskutieren. So kann ich der Gemeinschaft etwas zurückgeben.&lt;/p&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper left"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/eding.jpg" alt="Bild: EdingCNC Logo" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Auch Dank an das Team von &lt;a href="https://edingcnc.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;EdingCNC&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, vor allem an Iwona und Pim für Ihre schnellen und ausführlichen Antworten auf Support-Tickets und Controller-Software bezogene Fragen.&lt;/p&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper left"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/stritzelberger.jpg" alt="Bild: Stritzelberger Logo" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Vielen Dank an Jo von &lt;a href="https://vakuumtisch.de/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Stritzelberger&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; für stundenlange Diskussionen über gute Lösungen für meinen Vakuumtisch und Zubehör.&lt;/p&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper left"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/vhf.jpg" alt="Bild: vhf logo" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Von &lt;a href="https://shop.vhf.de/catalogs/Werkzeuge-W.htm?showStartpage=true" target="_blank" rel="noopener noreferrer" class="external-link"&gt;vhf&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; möchte ich mich bei Frau Dressner für die Zeit und die häufigen Gespräche über Schnittqualität, Optimierung der Vorschübe und Geschwindigkeiten für Schaftfräser des Unternehmens sowie Beratung über Materialfrässtrategien bedanken.&lt;/p&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper left"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/benezan.jpg" alt="Bild: Benezan-Logo" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Ein weiterer Dank geht an Nicolas von &lt;a href="https://www.benezan-electronics.de/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Benezan Electronics&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; für seine Unterstützung bei der Analyse eines Problems mit dem Signal-Breakout-Board und schließlich dessen Austausch.&lt;/p&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper left"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/gmc.jpg" alt="Bild: Gossen Metrawatt Logo" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Dank an Herrn Leibold von &lt;a href="https://www.gmc-instruments.de/en/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Gossen Metrawatt&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; für seine Hilfe bei der Auswahl eines guten Instruments zur Messung von Spannungsspitzen und der Leistungsaufnahme des Systems.&lt;/p&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper left"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/spinogy.jpg" alt="Bild: Spinogy-Logo" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Vielen Dank an Dominik von &lt;a href="https://www.spinogy.de/en/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Spinogy&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, mit dem ich über ein halbes Jahr lang telefoniert habe, um ein geeignetes Spindel-Upgrade und Zubehör für meine Maschine auszuwählen.&lt;/p&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper left"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/cadascam.jpg" alt="Bild: CadAsCam Logo" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Abschließend möchte ich mich bei Achim von &lt;a href="https://www.cadascam.com/de/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;CadAsCam&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; bedanken, für die freundliche Aufnahme von Software-Verbesserungsvorschlägen, die Beantwortung grundlegender Fragen und die extrem schnelle Reaktion mit Software-Updates und Korrekturen. Außerdem war euer kostenloser 2-Tages-Workshop große Klasse!&lt;/p&gt;&#10;&lt;h2 id="laufzeit-und-aufträge"&gt;Laufzeit und Aufträge&lt;/h2&gt;&#10;&lt;p&gt;Innerhalb eines Jahres habe ich 200 Aufträge mit einer Gesamtlaufzeit von knapp 18 Stunden gefertigt. Wenn ich Einrichten der Fräse auf die Jobs, Montage des Werkstücks und so weiter einbeziehe, kann man die Stundenzahl verdreifachen. Bezieht man zusätzlich meine Arbeitsstunden in CAD und CAM mit ein, wäre die Zahl locker nochmal zu verdoppeln - Man sieht also, dass so ein Hobby zu einem wahren Zeitfresser werden kann - umso wichtiger ist es dann, dass es Spaß macht.&lt;/p&gt;&#10;&lt;p&gt;Wie viel Zeit mich Aufbau, Wartung und Pflege der Maschine gekostet haben? Ich weiß es nicht genau. Aber ich schätze, dass es auf Augenhöhe liegt mit dem Betreiben dieser Website, wo die Erstellung von Inhalten für eine Minute Lesedauer mich in etwa eine Stunde in Beschlag nimmt.&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/1year_cnc/jobs_1year.jpg" alt="Bild: CNC-Softwareanzeige" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Außerdem habe ich viele Stunden damit verbracht, online nach Materialmessgeräten und Fräsern zu suchen, Ratschläge in Foren einzuholen und zu erteilen, nach Maschinen-Upgrades zu suchen und Videos darüber anzusehen, wie andere Leute ihre CNCs benutzen.&lt;/p&gt;&#10;&lt;p&gt;Mein derzeitiger Favorit (Stand: März 2023) ist übrigens von CNC Connect (&lt;a href="https://www.youtube.com/watch?v=iqNTEnlXwO0" target="_blank" rel="noopener noreferrer" class="external-link"&gt;youtube link&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;). Ich werde nicht versuchen, mich damit zu vergleichen - denn ich bin ja kein Profi - aber ich bin immer wieder beeindruckt von Menschen und Herstellern, die ihr Handwerk verstehen.&lt;/p&gt;&#10;&lt;h2 id="kosten"&gt;Kosten&lt;/h2&gt;&#10;&lt;p&gt;Das CNC-Hobby ist &lt;em&gt;teuer&lt;/em&gt;, sowohl in Bezug auf Geld als auch auf Zeit. In meinem Fall habe ich fast 13.000,00€ 😨 in die Maschine, ihr Zubehör, die Spindel, den Maschinentisch, die Stromversorgung und die Schutzausrüstung, die Beleuchtung, die numerische Steuerung, den Laptop, die CAD/CAM/CNC-Software, die Schalldämmung, die Werkzeuge zum Bau der Maschine, die Absauganlage, die persönliche Sicherheitsausrüstung usw. investiert.&lt;/p&gt;&#10;&lt;p&gt;Und dabei habe ich noch nicht einmal die Kosten für Schaftfräser erwähnt.&lt;/p&gt;&#10;&lt;p&gt;Kurz vor dem Kauf denkt man: &amp;ldquo;Die Maschine an sich ist nicht allzu teuer&amp;rdquo;. Aber sobald man sich zum Kauf entschließt, werden sofort weitere Dinge erforderlich. Ich habe fast immer &amp;ldquo;B&amp;rdquo; gesagt nachdem ich &amp;ldquo;A&amp;rdquo; rief, bin nur wenige Kompromisse bei der Qualität der Ausrüstung eingegangen und habe mich nicht auf ein Material spezialisiert, was die Kosten geringer gehalten hätte.&lt;/p&gt;&#10;&lt;p&gt;Ich war fasziniert von den verschiedenen Materialien und ihren Eigenschaften, und es war immer eine Freude, wenn ein Teil wirklich gut wurde, unabhängig von dem Material, aus dem es bestand.&lt;/p&gt;&#10;&lt;h2 id="misserfolge-und-lösungen"&gt;Misserfolge und Lösungen&lt;/h2&gt;&#10;&lt;p&gt;Hier ist eine Liste größerer Probleme, die ich im letzten Jahr erlebt habe.&lt;/p&gt;&#10;&lt;h3 id="oberfräsenmotor-fällt-aus-bzw-wackelkontakt-im-betrieb-"&gt;Oberfräsenmotor fällt aus bzw. Wackelkontakt im Betrieb. Ⓜ️&lt;/h3&gt;&#10;&lt;p&gt;Dieses Problem war in Hartholz und Multiplex nach nur 8h Spindelbetrieb reproduzierbar. Die Lösung war der Einbau einer neuen Elektronikplatine, die Maffell kurzerhand lieferte.&lt;/p&gt;&#10;&lt;h3 id="die-geschwindigkeit-des-oberfräsenmotors-fällt-mitten-im-auftrag-auf-die-mindestdrehzahl-"&gt;Die Geschwindigkeit des Oberfräsenmotors fällt mitten im Auftrag auf die Mindestdrehzahl Ⓜ️&lt;/h3&gt;&#10;&lt;p&gt;Dieses Problem war ebenfalls reproduzierbar und hatte zwei Ursachen:&lt;/p&gt;&#10;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/emergency.jpg" alt="Bild: Notschalter-Detailansicht" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Der Not-Aus-Schalter hatte einen geringen Auslöseweg, so dass kurze Kontaktprellungen möglich waren. Der Schalter ist auf dem Maschinentisch montiert und konnte durch Maschinenvibrationen ausgelöst werden. Ich habe dies behoben, indem ich die Position des Mikroschalters im Gehäuse so verändert habe, dass der Schalter nicht vorzeitig ausgelöst wird.&lt;/li&gt;&#10;&lt;li&gt;Auf der Breakout-Platine, die das analoge &lt;code&gt;0-10V&lt;/code&gt;-Signal aus dem Pulsbreitenbefehl der CNC erzeugt, war ein falscher Operationsverstärkertyp montiert (nicht RTR). Unter noch unbekannten Umständen (vielleicht Kontaktprellen des Notschalters?) kam es bei diesem Teil zu einem &amp;ldquo;&lt;a href="https://en.wikipedia.org/wiki/Latch-up" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Latch-up&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&amp;rdquo;, so dass die Geschwindigkeit des Oberfräsenmotors auf die niedrigste Einstellung fiel.&#10;Ein neues Signal-Breakout-Board wurde montiert und das Problem war damit behoben.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="velocityerror"&gt;CNC Meldung: &amp;ldquo;The Velocity was higher than max!&amp;rdquo; ⚠️&lt;/h3&gt;&#10;&lt;p&gt;Dieser Fehler trat bei einigen Gelegenheiten auf. Sie waren alle unangenehm. Einmal habe ich meine Maschine zu schnell bewegen lassen (gleichzeitig &lt;code&gt;240mm/s&lt;/code&gt; auf XY, &lt;code&gt;110mm/s&lt;/code&gt; auf Z), was dazu führte, dass sie quietschte und eine Menge Schritte verlor, die sich in den Zentimeterbereich summierten.&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/1year_cnc/stepfrequency_old.jpg" alt="Bild: Übertaktete Maschinenkinematik, die zu Problemen führte" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Ein anderes Mal, als ich weniger hohe (aber im Nachhinein immer noch zu hohe) Geschwindigkeitswerte eingegeben hatte, machte ich einen Fehler, indem ich Eilgang &amp;ldquo;G00 Xxx Yyy Zzz&amp;rdquo; für alle Achsen gleichzeitig befahl, was dazu führte, dass die Maschine beim Abbremsen Schritte verlor und auf der Z-Achse an den mechanischen Endanschlag stieß. Glücklicherweise war es die obere Anschlag, so dass weder die Spindel noch die Maschine bzw. Maschinenbett beschädigt wurden. Nur die Schrittmotorkupplung rutschte durch. Uff.&lt;/p&gt;&#10;&lt;p&gt;Der Grund dafür: Die maximale Impulsfrequenz für &lt;em&gt;kombinierte Achsenbewegungen&lt;/em&gt; beträgt bei meinem Controller &lt;code&gt;125kHz&lt;/code&gt;. Ich dachte, das sei pro Achse. Das ist &lt;em&gt;falsch&lt;/em&gt;. Ich habe das mit dem Hersteller der numerischen Steuerung abgeklärt. Ich musste die Geschwindigkeitswerte der Achsen neu einstellen, um &lt;em&gt;kombiniert&lt;/em&gt; unter &lt;code&gt;125kHz&lt;/code&gt; zu bleiben.&lt;/p&gt;&#10;&lt;p&gt;Um neue Werte zu finden, habe ich mir meine bisherigen Aufträge angesehen. Ich verliere mehr Zeit bei schnellen Bewegungen der Z-Achse als bei den anderen beiden Achsen. Deshalb habe ich das Hauptaugenmerk auf die Z-Achse gelegt, um sie schnell und mit hoher Beschleunigung zu fahren, während ich die Werte für die X- und Y-Achse reduziert habe. Dabei ist zu beachten, dass die Z-Achse aufgrund ihres kürzeren Kugelumlaufgewindes auf meiner Maschine die doppelte Auflösung hat, so dass die Werte verdoppeln müssen, um sie mit den anderen Achsen vergleichen zu können.&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/1year_cnc/stepfrequency_new.jpg" alt="Bild: Abgestimmte Maschinenkinematik zur Behebung des Problems" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Dies alles geschah nach der Verbesserung der Fertigungsqualität, die ich &lt;a href="https://blog.schallbert.de/cut-dibond-tests/#machine-vibrations"&gt;hier&lt;/a&gt; dokumentiert habe. Diese Werte liefern also nachweislich gute Ergebnisse und werden von mir nicht mehr leichtfertig verändert.&lt;/p&gt;&#10;&lt;h3 id="kollision-der-maschine-mit-dem-vakuumtisch-"&gt;Kollision der Maschine mit dem Vakuumtisch 💥&lt;/h3&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/1year_cnc/vacuumtable_cut.jpg" alt="Bild: 6mm Frässpuren in meinem brandneuen Vakuumtisch" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Nun, das hat mich wirklich verärgert. Ich hatte den Tisch noch nicht einmal zwei Wochen, und schon jetzt hatte ich ihn beschädigt. Das Schlimmste war, dass ich nicht verstand, warum.&lt;/p&gt;&#10;&lt;p&gt;Alles, was ich wollte, war &lt;code&gt;18mm&lt;/code&gt; Birke Multiplex zu fräsen. Ich ließ den Schruppfräser den Umriss erstellen, sah mich aber mit einem Problem konfrontiert, das mich dazu brachte, den Not-Aus zu drücken, als der Vorgang bereits abgeschlossen war (beim Zurückfahren zu XY0 wäre der Fräser auf ein Hindernis gestoßen). Ich korrigierte den Pfad und setzte die Maschine zurück. Ich wechselte das Werkzeug zum Schlichten und startete den Auftrag.&lt;/p&gt;&#10;&lt;p&gt;Als ich merkte, dass die Maschine ungewöhnliche Geräusche von sich gab, betätigte ich den Not-Aus. Aber bei über &lt;code&gt;4000mm/min&lt;/code&gt; zum Schlichten war ich nicht schnell genug, um zu verhindern, dass die Maschine durch den Vakuumtisch fräst.&lt;/p&gt;&#10;&lt;p&gt;Ich schaute mir den Arbeitsversatz für die Z-Achse im Variablenspeicher für &lt;code&gt;G92&lt;/code&gt; an. Er war korrekt. Ich suchte in den flüchtigen Variablen der CNC. Alle stimmten mit meinen Erwartungen überein. Dann begann ich, in den Logdateien der Maschine zu suchen. Glücklicherweise habe ich das obige Bild direkt nach diesem Vorfall gemacht, so dass es nicht schwer war, die richtigen Protokollzeilen herauszufinden.&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/1year_cnc/edinglog.jpg" alt="Bild: Logdatei mit Werkzeuglängenmessungen" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Man kann erkennen, dass die Werkzeuglänge sowohl für den Schrupp- als auch für den Standardfräser gemessen wurde. Der Standardfräser ragt um etwa 3,4 mm weiter heraus. Das ist genau die Tiefe, die mir jetzt auf meinem Vakuumtisch fehlte.&lt;/p&gt;&#10;&lt;p&gt;Dennoch hatte ich keine Ahnung, warum die Maschine die aktualisierte Werkzeuglänge nicht berücksichtigte. Also habe ich mir das Makro für den Werkzeugwechsel angesehen.&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/1year_cnc/macro_rootcause.jpg" alt="Bild: Makro mit Werkzeuglängenmessroutine" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Man kan sehen, dass Variable &lt;code&gt;#3501&lt;/code&gt; speichert, ob bereits eine Werkzeuglänge gemessen wurde. Wenn dies der Fall ist, kann einfach die neue Werkzeuglänge gemessen und die Differenz zwischen den beiden Werten als Z-Achsen-Korrektur gespeichert werden. Dann wird das Programm mit dem neuen Werkzeug fortgesetzt.&lt;/p&gt;&#10;&lt;p&gt;Das Problem ist, dass die Variable &lt;code&gt;#3501&lt;/code&gt; flüchtig ist, wie ein Blick in das Handbuch der Steuerung zeigt. Und es scheint, dass flüchtige Variablen nicht nur zurückgesetzt werden, wenn die Anwendung geschlossen wird, sondern auch, wenn der Not-Aus gedrückt wird.&lt;/p&gt;&#10;&lt;p&gt;Ausgehend von meinen vorherigen Aussagen hatte ich das folgende Problem: Das Drücken des Not-Aus löschte den Wert dieser Variablen, behielt aber die ursprüngliche Z0-Höhenmessung bei, die ich ganz am Anfang vor dem Einsatz des Schruppwerkzeugs vorgenommen hatte. Anstatt also die Höhenkorrektur mit dem neuen Werkzeug zu aktualisieren, um der neuen Werkzeuglänge Rechnung zu tragen, dachte das Makro, dass dies das erste verwendete Werkzeug sei &lt;em&gt;und speicherte&lt;/em&gt; einfach dessen Länge in der Variablen &lt;code&gt;#4501&lt;/code&gt; (weil man - logisch - keine Differenz nehmen kann, wenn man vor dem ersten Werkzeug kein Werkzeug vermessen hatte).&lt;/p&gt;&#10;&lt;p&gt;Das allein hätte den Vorfall gar nicht verursacht, wenn der neu eingesetzte Fräser nicht zufällig weiter aus der Spannzange herausgeragt hätte als der vorherige.&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/1year_cnc/macro_solution.jpg" alt="Bild: Zwischenlösung war die Änderung des Makros" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Ich löste das Problem, indem ich dem Makro eine Meldung hinzufügte, wenn es nur die Werkzeuglänge speicherte, aber nicht den Versatz der Z-Achse änderte. Auf diese Weise habe ich zumindest einen Hinweis darauf, dass etwas schief gehen könnte.&lt;/p&gt;&#10;&lt;h2 id="schlussfolgerung"&gt;Schlussfolgerung&lt;/h2&gt;&#10;&lt;p&gt;Zusammenfassend kann ich sagen, dass ich die CNC weiterhin benutzen werde. Ich werde nach und nach weiter dazulernen. Und ich werde mein Wissen darüber vertiefen, wie Software mit Mechatronik interagiert, die physikalische Materie verändert, wie es &amp;lsquo;Zerspanobert&amp;rsquo; tut. Ich werde versuchen, aufzuschreiben, was ich gelernt habe, damit andere meinen Spuren folgen können, wenn sie wollen.&lt;/p&gt;&#10;&lt;p&gt;Ich hoffe, dass ich einen guten Kontakt zu anderen Bastlern, Herstellern und zu den Firmen, mit denen ich bisher zusammenarbeiten durfte, aufrechterhalten kann (und vielleicht noch einige weitere zu meiner Liste hinzufügen können werde).&lt;/p&gt;&#10;&lt;p&gt;Bis bald!&#10;&lt;em&gt;Schallbert&lt;/em&gt;&lt;/p&gt;&#10;</description></item><item><title>AnywhereAmps - Alpha</title><link>https://blog.schallbert.de/projects/aa_alpha/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/aa_alpha/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/front-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: AnywhereAmps Alpha&amp;#39;s insides"&#10; title="AnywhereAmps - Alpha" /&gt;&#10;&lt;p&gt;Mobiler Instrumentenverstärker/Combo-Prototyp basierend auf &lt;a href="https://blog.schallbert.de/shelly13/"&gt;Shelly13&lt;/a&gt;. AnywhereAmps Alpha zeichnet sich durch einen geringen Stromverbrauch und eine kleine Stellfläche aus. Er ist als sehr tragbarer Vollbereichs-Übungsverstärker mit einem Gesamtgewicht von nur 3,1 kg konzipiert. Er verfügt über eine einstellbare Verstärkung und eine 9V DC-Versorgung für externe Effektpedale.&lt;/p&gt;&#10;&lt;h2 id="projekt-statistiken"&gt;Projekt-Statistiken&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Schwierigkeit: Amateur 3/5&lt;/li&gt;&#10;&lt;li&gt;Kosten: ~170€&lt;/li&gt;&#10;&lt;li&gt;Zeitaufwand: 10h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="impressionen"&gt;Impressionen&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="Bild: AnywhereAmps Alpha, aufrecht stehend."&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/upright.jpg" alt="Bild: AnywhereAmps Alpha, aufrecht stehend." class="hugo-gallery__thumb-img" loading="lazy" /&gt;&#10; &lt;/a&gt;&lt;a class="hugo-gallery__thumb" href="#gallery-full-1" aria-label="Bild: AnywhereAmps Alpha, mit Spannungsversorgung für Bodeneffektpedale"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/effects_on.jpg" alt="Bild: AnywhereAmps Alpha, mit Spannungsversorgung für Bodeneffektpedale" class="hugo-gallery__thumb-img" loading="lazy" /&gt;&#10; &lt;/a&gt;&lt;a class="hugo-gallery__thumb" href="#gallery-full-2" aria-label="Bild: AnywhereAmps Alpha eingeschaltet, Fokus auf die Bedienelemente."&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/switched_on.jpg" alt="Bild: AnywhereAmps Alpha eingeschaltet, Fokus auf die Bedienelemente." 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: AnywhereAmps Alpha, top view"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/top.jpg" alt="Image: AnywhereAmps Alpha, top view" class="hugo-gallery__thumb-img" loading="lazy" /&gt;&#10; &lt;/a&gt;&lt;a class="hugo-gallery__thumb" href="#gallery-full-4" aria-label="Image: AnywhereAmps Alpha, side view"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/side.jpg" alt="Image: AnywhereAmps Alpha, side view" class="hugo-gallery__thumb-img" loading="lazy" /&gt;&#10; &lt;/a&gt;&lt;a class="hugo-gallery__thumb" href="#gallery-full-5" aria-label="Image: AnywhereAmps Alpha Amplifier Boards"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/amp.jpg" alt="Image: AnywhereAmps Alpha Amplifier Boards" class="hugo-gallery__thumb-img" loading="lazy" /&gt;&#10; &lt;/a&gt;&lt;/div&gt;&lt;div class="hugo-gallery__caption"&gt;Impressionen vom Einsatz meines AnywhereAmp Alpha.&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/aa_alpha/upright.jpg" alt="Bild: AnywhereAmps Alpha, aufrecht stehend." class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__figcaption"&gt;Alpha kann auch aufrecht gespielt werden, ideal in kleinen Räumen&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/aa_alpha/effects_on.jpg" alt="Bild: AnywhereAmps Alpha, mit Spannungsversorgung für Bodeneffektpedale" class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__figcaption"&gt;9V Spannungswandler für Bodeneffektpedale&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/aa_alpha/switched_on.jpg" alt="Bild: AnywhereAmps Alpha eingeschaltet, Fokus auf die Bedienelemente." class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__figcaption"&gt;Minimalistische Bedienelemente: Ein/Aus Schalter, Verstärkung, 6,3mm Klinke Eingang, 9V Ausgang&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/aa_alpha/top.jpg" alt="Image: AnywhereAmps Alpha, top view" class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__figcaption"&gt;31cm Durchmesser, 9l Volumen&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure id="gallery-full-4" class="hugo-gallery__figure"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/side.jpg" alt="Image: AnywhereAmps Alpha, side view" class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__figcaption"&gt;Akku auf der Rückseite montiert, zum Abnehmen nach oben ziehen, Restkapazität leicht ablesbar&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure id="gallery-full-5" class="hugo-gallery__figure"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/amp.jpg" alt="Image: AnywhereAmps Alpha Amplifier Boards" class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__figcaption"&gt;Vor- und Endstufe sind übereinander angeordnet&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&#10;&lt;/section&gt;&#10;&lt;h2 id="amp-sound-samples"&gt;Klangbeispiele&lt;/h2&gt;&#10;&lt;h3 id="bass-samples"&gt;Bass samples&lt;/h3&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;audio controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/audio/aa_alpha_fingered_highnotes.mp3" type="audio/mp3"&gt;&#10; Your browser does not support the audio element.&#10; &lt;/audio&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Hauptsächlich höhere Töne&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;audio controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/audio/aa_alpha_fingered_playedhard.mp3" type="audio/mp3"&gt;&#10; Your browser does not support the audio element.&#10; &lt;/audio&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Hart angeschlagen&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;audio controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/audio/aa_alpha_fingered_flageolet.mp3" type="audio/mp3"&gt;&#10; Your browser does not support the audio element.&#10; &lt;/audio&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Leersaiten &amp;amp; Flageoletts&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Alle Beispiele aufgenommen mit dem H4n von Zoom, Bass: &lt;a href="http://www.hoshinogakki.co.jp/pdf/ibanez/catalog/1992Supplement_USA.pdf" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Ibanez SR1500PD&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; gebaut 1992, Tonblende auf Hals-Pickups, Lautstärke-Potentiometer voll aufgedreht.&lt;/p&gt;&#10;&lt;h3 id="abmessungen"&gt;Abmessungen&lt;/h3&gt;&#10;&lt;p&gt;Trommelkessel: &lt;code&gt;12 &amp;quot;x6&amp;quot;&lt;/code&gt;. Die Gesamthöhe beträgt etwa 17 cm, die Breite 30 cm und die Tiefe (ohne Batterie) 32 cm. Das umschlossene Volumen inkl. Verstärker und Lautsprecher beträgt &lt;code&gt;~9l&lt;/code&gt;. Ich verwende geschlossenes Gehäusedesign.&lt;/p&gt;&#10;&lt;h3 id="sound-hardware"&gt;Sound Hardware&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Lautsprecher: &lt;a href="https://www.visaton.de/en/products/drivers/woofers/kt-100-v-4-ohm" target="_blank" rel="noopener noreferrer" class="external-link"&gt;4&amp;quot; Basslautsprecher&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und zwei &lt;a href="https://www.monacor.com/products/components/speaker-technology/pa-tweeters-and-horn-drivers-/mpt-005/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Piezo-Hochtöner&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://store.sure-electronics.com/product/AA-AB32996" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Class D-Verstärker&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; mit &lt;code&gt;30W&lt;/code&gt; Leistung (gebrückt)&lt;/li&gt;&#10;&lt;li&gt;14,4V Li-Ion Werkzeugakku&lt;/li&gt;&#10;&lt;li&gt;Alle empfindlichen Teile befinden sich im Inneren des Gehäuses&lt;/li&gt;&#10;&lt;li&gt;Speziell angefertigter Vorverstärker mit &lt;code&gt;0...27dB&lt;/code&gt; einstellbarer Verstärkung&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="simulation"&gt;Simulation&lt;/h3&gt;&#10;&lt;p&gt;Die Simulation zeigt, dass die Lautstärke des ~9l gedämpften Gehäuses perfekt zu dem gewählten 4&amp;quot; Lautsprecher passt. Sie reicht bis auf &lt;code&gt;44Hz&lt;/code&gt; hinunter (F#1, ein Ganzton oberhalb der tiefsten offenen Saite eines (4-Saiter) Basses, E1). Es gibt druckvolle Bässe bei noch guter Präzision. Der Nachteil ist der geringe Wirkungsgrad von nur &lt;code&gt;78 dB/W/m&lt;/code&gt;, so dass die zu erwartende Maximallautstärke bei der Leistungsaufnahme eher gering ist und der Lautsprecher früh an seine Auslenkungsgrenzen kommt.&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/aa_alpha/simulation.jpg" alt="AA09_1421_30 Sim" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h2 id="vorverstärker-im-eigenbau"&gt;Vorverstärker im Eigenbau&lt;/h2&gt;&#10;&lt;p&gt;Der von mir gewählte Class-D-Verstärker benötigt einen Line-Pegel-Eingang, so dass ich einen Vorverstärker in mein Design einbauen musste, um eine ausreichende Lautstärke aus der Endstufe zu erhalten. Selbst die billigsten Vorverstärker wie &lt;a href="https://artproaudio.com/product/tube-mp-the-original/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;dieser von Art Pro Audio&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; würden das Design erheblich verteuern. Sie bringen recht viel auf die Waagen und benötigen recht viel Energie, vor allem, wenn es sich um Röhrenvorverstärker handelt. Sie haben oft spezielle Anforderungen an die Eingangsspannung (z. B. nur Wechselstrom). Außerdem würde sich ihr Design nicht in meins einfügen.&lt;/p&gt;&#10;&lt;p&gt;Also beschloss ich, selbst eine Verstärkerstufe mit geringem Stromverbrauch aus möglichst wenigen Bauteilen zu entwickeln. &lt;a href="https://blog.schallbert.de/projects/aa_alpha/#sound-quality"&gt;Hier klicken um die technischen Details zu überspringen&lt;/a&gt;&lt;/p&gt;&#10;&lt;h3 id="spannungsschienen"&gt;Spannungsschienen&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/aa_alpha/virtualground.jpg" alt="Bild: Virtuelle Masseschaltung" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Da der Vorverstärker Wechselspannungssignale verstärken soll, ich aber nur eine Batteriespannungsschiene zur Verfügung habe, musste ich mir zunächst überlegen, wie ich ein &lt;a href="https://en.wikipedia.org/wiki/Virtual_ground" target="_blank" rel="noopener noreferrer" class="external-link"&gt;virtuelles Masse&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;potenzial schaffen kann, das den Nullpunkt des Signals für die Verstärkungsstufe darstellt. Glücklicherweise gibt es eine Menge &lt;a href="https://mil.ufl.edu/4924/docs/TI_SingleSupply_OpAmp.pdf" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Design-Richtlinien&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; darüber, wie man den Verstärker mit einer einzigen Spannungsquelle betreibt, so dass ich nicht bei Null anfangen musste.&lt;/p&gt;&#10;&lt;h3 id="verstärkungsstufe"&gt;Verstärkungsstufe&lt;/h3&gt;&#10;&lt;p&gt;AnywhereAmps Alpha sollte ein Mehrzweck-Instrumentencombo sein, so dass ein breiter Verstärkungsbereich zur Verfügung stehen sollte, um alles von einer passiven Gitarre bis zu einem Line-Level-Keyboard auf Spannungspegel zu bringen, mit denen die Endstufe umgehen kann. Deshalb habe ich mich für einen &lt;a href="http://www.sengpielaudio.com/calculator-levelchange.htm" target="_blank" rel="noopener noreferrer" class="external-link"&gt;0-27dB Verstärkungsbereich&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; entschieden und als Widerstände für die Rückkopplungsschleife &lt;code&gt;10kOhm&lt;/code&gt; und ein &lt;code&gt;250kOhm&lt;/code&gt; logarithmisches Potentiometer gewählt.&#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/aa_alpha/gainstage.jpg" alt="Schaltung der Verstärkungsstufe" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&lt;/p&gt;&#10;&lt;h3 id="schaltungssimulation"&gt;Schaltungssimulation&lt;/h3&gt;&#10;&lt;p&gt;Ich habe &lt;a href="https://www.analog.com/en/design-center/design-tools-and-calculators/ltspice-simulator.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;LTspice&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; von Analog Devices verwendet um zu simulieren, wie sich diese Schaltung bei einem typischen Audiosignal zwischen &lt;code&gt;20H&lt;/code&gt;z und &lt;code&gt;20kHz&lt;/code&gt; verhalten könnte. Aus meinem Studium erinnere ich mich daran, dass mich ein so genannter Bode-Plot am meisten interessieren müsste. Dieser kann sowohl Amplitude als auch Phasenlage über der Frequenz in einer doppelt-logarithmischen Skala darstellen. Praktischerweise bietet LTspice eine solche &lt;a href="https://www.analog.com/en/technical-articles/how-to-generate-a-bode-plot-with-ltspice.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Funktion&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&#10;Wie man sieht, bleibt der Ausgang des Vorverstärkers - grün - bis in den &lt;code&gt;10kHz&lt;/code&gt;-Bereich flach und die Phase verschiebt sich nur geringfügig, während das Eingangssignal - blau - &lt;code&gt;~27dB&lt;/code&gt; darunter bleibt.&#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/aa_alpha/bodeplot.jpg" alt="Bild: Verstärkerschaltung" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&lt;/p&gt;&#10;&lt;h3 id="op-amp-auswahl"&gt;Op-Amp Auswahl&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/aa_alpha/crossover_distortion.jpg" alt="Bild: Übergabeverzerrung" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Tja. Ich habe dieses Design auf meinem Steckbrett mit einem Mehrzweck-&lt;a href="https://en.wikipedia.org/wiki/Operational_amplifier" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Operationsverstärker&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; den ich herumliegen hatte, aufgebaut. Das klappte&amp;hellip; nicht besonders gut. Als ich einen Funktionsgenerator am Eingang anschloss und am Ausgang des &lt;a href="https://www.mikrocontroller.net/part/LM324" target="_blank" rel="noopener noreferrer" class="external-link"&gt;LM324&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; eine Sinuswelle erwartete, erhielt ich am Ausgang überhaupt keine Sinuswelle, sondern einen Halbsinus mit einigen Stufen und entladungsähnlichen Kurven um &lt;code&gt;0&lt;/code&gt;, gefolgt von einem negativen Halbsinus&amp;hellip; Nach einiger Suche fand ich heraus, dass dies &amp;ldquo;&lt;a href="https://en.wikipedia.org/wiki/Crossover_distortion" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Übergabeverzerrung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&amp;rdquo; genannt wird und durch die asymmetrische Ausgangsstufe dieses speziellen Operationsverstärkertyps verursacht wird. Ich konnte den Effekt ein wenig reduzieren, indem ich einen höheren Laststrom am Ausgang anlegte. Aber letztendlich wollte ich einen Operationsverstärker nehmen, der für meine Audioanwendung ohne Rail-to-Rail-Fähigkeit und mit höherem Rauschabstand besser geeignet war. Zum Glück gibt es einige Seiten zu diesem Thema wie &lt;a href="https://www.cycfi.com/projects/six-pack/op-amp-shootout/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Cicfy Research&amp;rsquo;s Op-Amp shootout&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Unter Abwägung eines vernünftigen Stückpreises und der Verfügbarkeit bei meinem Elektronikhändler habe ich mich für den &lt;a href="https://www.ti.com/lit/ds/symlink/ne5532.pdf" target="_blank" rel="noopener noreferrer" class="external-link"&gt;NE5532P&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; entschieden. Das folgende Video zeigt, was ich mit der aktualisierten Op-Amp-Auswahl erreicht habe.&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/aa_alpha/preamp_working.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;Sinuswelle am Vorverstärker&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h3 id="schaltungsentwurf"&gt;Schaltungsentwurf&lt;/h3&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/circuit_board.jpg" alt="Bild: Schaltplan AnywhereAmps Alpha" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt; Ich wollte, dass meine Platine so wenig Platz wie möglich beansprucht, während die Leiterbahnen für die Gleichspannungsversorgung wie ein Rahmen an den Außenseiten der Platine liegen. Die linke Hälfte der Platine ist für die Erzeugung des virtuellen Massepotentials zuständig, während die rechte Hälfte den Verstärkungsteil übernimmt. Das Design ist minimalistisch und verwendet nur vier Widerstände und drei Kondensatoren.&lt;/p&gt;&#10;&lt;p&gt;Einen tieferen Einblick in die Technik erhalten Sie in meinen &lt;a href="https://blog.schallbert.de/preamp-design-considerations/"&gt;Design-Überlegungen&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="sound-quality"&gt;Klangqualität&lt;/h2&gt;&#10;&lt;p&gt;Der AnywhereAmps Alpha ist im Leerlauf absolut leise, selbst bei voll aufgedrehter Verstärkung ist nur ein leichtes Zischen der Hochtöner zu vernehmen. Kein Brummen, kein Kratzen beim Drehen des Gain-Reglers. Dank des weiten Gain-Bereichs von &lt;code&gt;0dB&lt;/code&gt; für Line-Pegel wie Keyboards, Preamp-Instrumente, Mobiltelefone und bis zu &lt;code&gt;+27dB&lt;/code&gt; für passive Bässe oder Gitarren-Direkteingänge funktioniert er mit jedem Instrument. Die maximale Lautstärke ist in Ordnung, obwohl die tiefen Töne zu früh &amp;ldquo;abflachen&amp;rdquo;, was den nutzbaren Lautstärkebereich z.B. für Orgeln und 5-saitige Bässe einschränkt. Für Frequenzen im Bassbereich bei hoher Lautstärke kommt es immer noch auf die Physik an - je größer, desto besser. Einige Klangbeispiele finden Sie &lt;a href="https://blog.schallbert.de/projects/aa_alpha/#amp-sound-samples"&gt;oben&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="zusammenfassung"&gt;Zusammenfassung&lt;/h2&gt;&#10;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/shell_raw.jpg" alt="Bild: Tom-Kessel" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Für das Gehäuse habe ich bei &lt;a href="http://cube-drums.de/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Cube Personal Drums&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; einige rohe Tom-Tom-Kessel in Sondergröße bestellt. Das Bohren und vor allem das Fräsen einer gekrümmten Oberfläche von Hand erwies sich als schwierig, und die Tatsache, dass ich keine richtige Holzwerkstatt besitze und die ganze Arbeit auf dem Balkon meiner Wohnung erledigte, machte es nicht einfacher.&#10;Aber nach ca. 10 Stunden inkl. Löt- und Elektronikarbeiten habe ich jetzt mehr als nur eine super-portable Instrumentenkombination.&lt;/p&gt;&#10;&lt;p&gt;Es ist ein Möbelstück.&lt;/p&gt;&#10;&lt;p&gt;Ich werde viel darüber spielen. Außerdem motiviert mich der gute Klang mehr denn je, den nächsten Schritt zu tun und die Wiedergabe des Tieftonbereichs mit einem weiteren, größeren Prototyp zu verbessern.&lt;/p&gt;&#10;&lt;h2 id="vorteile"&gt;Vorteile:&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Ich mag das Design.&lt;/li&gt;&#10;&lt;li&gt;Passt in Standard-Tom-Taschen!&lt;/li&gt;&#10;&lt;li&gt;Leichtgewicht, &lt;code&gt;3.1kg&lt;/code&gt; inkl. Batterie, &lt;code&gt;2.6kg&lt;/code&gt; ohne&lt;/li&gt;&#10;&lt;li&gt;Ladezustand der Batterie kann auf einen Blick abgelesen werden&lt;/li&gt;&#10;&lt;li&gt;Verdeckte Bedienelemente: Vermeidet ungewollte Bedienung&lt;/li&gt;&#10;&lt;li&gt;Noch robuster: Die gesamte Elektronik und das Lautsprecherchassis sind im Inneren montiert&lt;/li&gt;&#10;&lt;li&gt;Inklusive &amp;ldquo;9V, 1.6A&amp;rdquo;-Stromversorgungsbuchse für Effekte, Pedale, etc.&lt;/li&gt;&#10;&lt;li&gt;Klingt gut für jedes Instrument, da es fast das gesamte hörbare Frequenzspektrum wiedergibt&lt;/li&gt;&#10;&lt;li&gt;Läuft &amp;gt;20h mit einer Batterieladung (14,4V, 4Ah) als Übungsverstärker bei moderater Lautstärke&lt;/li&gt;&#10;&lt;li&gt;Läuft &amp;gt;3 Stunden mit einer Batterieladung bei maximaler Lautstärke&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="unvollkommenheiten"&gt;Unvollkommenheiten:&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Zu klein für Subbass-Wiedergabe.&lt;/li&gt;&#10;&lt;li&gt;Keine automatische Stummschaltung beim Ein- und Ausschalten des Verstärkers -&amp;gt; Pop-Noise&lt;/li&gt;&#10;&lt;li&gt;Natürlich nicht genug Lautstärke, um ein Schlagzeug oder ein Plugged-Band-Setup zu bedienen.&lt;/li&gt;&#10;&lt;li&gt;Sieht komisch aus mit den großen Batteriepacks auf der Rückseite. Die Verwendung eines &amp;ldquo;Flat Pack&amp;rdquo; wäre schön&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="anywhereamps-alpha-diy-replik"&gt;AnywhereAmps Alpha: DIY Replik&lt;/h2&gt;&#10;&lt;p&gt;Eigenbau? Klar, gern. Unten ist die Schritt-für-Schritt-Anleitung verlinkt!&#10;&lt;a href="https://blog.schallbert.de/anywhereamps-diy/"&gt;AnywhereAmps Alpha DIY Anleitung&lt;/a&gt;&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/aa_alpha/anywhereamps_alpha_fulldemo.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;Alpha live!&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>AnywhereAmps - Alpha 1.1</title><link>https://blog.schallbert.de/projects/aa_alpha_1_1/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/aa_alpha_1_1/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha11/top-thumb.jpg"&#10; class="post-cover"&#10; alt="AnywhereAmps Alpha 1.1: teaser"&#10; title="AnywhereAmps - Alpha 1.1" /&gt;&#10;&lt;p&gt;Mobiler Instrumenten-Verstärker/Cabinet-Kombi-Prototyp Version 1.1, entwickelt aus &lt;a href="https://blog.schallbert.de/projects/aa_alpha/"&gt;AnywhereAmps Alpha&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="projekt-statistiken"&gt;Projekt-Statistiken&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Schwierigkeit: Amateur 3/5&lt;/li&gt;&#10;&lt;li&gt;Kosten: ~200€ plus Batteriesystem&lt;/li&gt;&#10;&lt;li&gt;Zeitaufwand: 3h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="aktualisierungen-gegenüber-alpha-10"&gt;Aktualisierungen gegenüber Alpha 1.0:&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Hinzufügen einer &lt;a href="https://blog.schallbert.de/configure-max1756x/"&gt;Stromüberwachungsschaltung&lt;/a&gt;, um sowohl den Verstärker als auch das Akkupack vor abnormalen Spannungen / Strömen zu schützen&lt;/li&gt;&#10;&lt;li&gt;Integrierter Ein/Aus-Schalter im Potentiometer&lt;/li&gt;&#10;&lt;li&gt;Geringere Komplexität durch Verwendung eines hocheffizienten Breitbandlautsprechers anstelle eines 2-Wege-Systems mit 3 Lautsprechern&lt;/li&gt;&#10;&lt;li&gt;Hinzufügen eines hocheffizienten Abwärtswandlers zur Erzeugung einer 9-V-Effektspannungsschiene&lt;/li&gt;&#10;&lt;li&gt;Geringere Bauzeit durch Verwendung einer &lt;a href="https://blog.schallbert.de/anywhereamps-get-pcb/"&gt;kundenspezifischen Leiterplatte&lt;/a&gt; mit Vorverstärker, Abwärtswandler und &amp;ldquo;Power Supervisor&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;Reduzierte Bauzeit, da Großteil der Holzarbeiten an eine Portalfräse (CNC&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;) ausgelagert werden konnte&lt;/li&gt;&#10;&lt;li&gt;Einschaltknackgeräusche eliminiert durch Hinzufügen eines RC-Filters zum Standby-Eingang der Endstufe&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="bauvarianten"&gt;Bauvarianten&lt;/h3&gt;&#10;&lt;p&gt;Um den Klang meines Prototyps weiter zu verfeinern, habe ich dieses Mal verschiedene Varianten gebaut. Ich habe die Chassis ausgetauscht und den Klang bewertet:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Alpha 1.0: 30W gebrückter Verstärker, 4&amp;quot; Bass und 2x Piezo-Hochtöner: HiFi-ähnlicher Sound mit druckvollen Bässen und klaren Höhen, limitiert in der maximalen Lautstärke&lt;/li&gt;&#10;&lt;li&gt;Alpha 1.1 v1: 30W gebrückter Verstärker, 4&amp;quot; PA-Treiber, gute Lautstärke und hohe Präsenz, aber unterrepräsentierte Bässe und Höhen&lt;/li&gt;&#10;&lt;li&gt;Alpha 1.1 v2: 30W gebrückter Verstärker, 4&amp;quot; PA-Treiber und Passivmembran, satte Bässe aber etwas dumpf in den Höhen&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="meine-eigene-platine-für-vorverstärker-und-zusatzfunktionen"&gt;Meine eigene Platine für Vorverstärker und Zusatzfunktionen&lt;/h2&gt;&#10;&lt;p&gt;Die &lt;a href="https://blog.schallbert.de/pcbs-arrived/"&gt;AnywhereAmps 1.1 PCB&lt;/a&gt; bietet neben dem reinen Vorverstärker plus Tiefpass und Hochpass für Bi-Amping noch einige Zusatzfunktionen. Die Details sind in den folgenden Abschnitten beschrieben.&lt;/p&gt;&#10;&lt;h3 id="power-supervisor"&gt;Power Supervisor&lt;/h3&gt;&#10;&lt;p&gt;Ich habe eine Power Supervisor-Schaltung hinzugefügt, um zu verhindern, dass die Batterie komplett leer gezogen wird. Außerdem wollte ich die Risiken durch Kurzschlüsse und andere Fehlerzustände, die die Batterie beschädigen könnten, verringern. Die Integration dieser Technologie funktionierte auf Anhieb (was ich nicht erwartet hatte). Der einzige Nachteil ist, dass der Verstärker bei einer Entladung der Batterie auf z. B. 10 % manchmal neu startet, wenn die Versorgungsspannung unter die Auslösespannung des Überwachungsgeräts fällt. Da die Batteriespannung wieder ansteigt, wenn die Last abfällt, schaltet er sich eine Sekunde später wieder ein.&lt;/p&gt;&#10;&lt;p&gt;Dies ist eindeutig ein &lt;a href="https://blog.schallbert.de/capacitor-problems/"&gt;unerwünschtes Verhalten&lt;/a&gt; und muss in einem weiteren, verbesserten Prototyp behandelt werden.&#10;Ein weiterer Punkt, der diesen Folgeprototypen notwendig macht, ist die Tatsache, dass ich in meinen zukünftigen Entwürfen keine Batterien des Werkzeugherstellers verwenden darf &lt;a href="https://blog.schallbert.de/no-can-use-metabo/"&gt;wie hier diskutiert&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="9v-abwärtswandler"&gt;9V Abwärtswandler&lt;/h3&gt;&#10;&lt;p&gt;Zur Erzeugung von zusätzlichen 9V für Geräte wie Effekte, Looper usw. verwende ich einen Schaltregler. Ich wollte das Rauschen reduzieren, indem ich einen &amp;ldquo;leisen&amp;rdquo; Schaltregler wählte. Der von mir gewählte Regler wurde für Anwendungen im Automobilbereich entwickelt und hat die Induktivitäten in seinem Gehäuse integriert. Von Nachteil ist, dass der Baustein kaum von Hand gelötet werden kann. Außerdem ist er sehr teuer.&lt;/p&gt;&#10;&lt;p&gt;Der Wandler schaltet sich ein, wenn ein Gerät in die entsprechende Buchse eingesteckt wird, so dass es keinen Standby-Strom gibt. Er arbeitet leise (aber nicht ganz rauschfrei) und bietet mehr als genug Leistung für die Anwendungen, die ich bisher habe - ein Looper, ein Kompressor und ein Tuner können problemlos parallel betrieben werden.&lt;/p&gt;&#10;&lt;h2 id="holzbearbeitung"&gt;Holzbearbeitung&lt;/h2&gt;&#10;&lt;p&gt;Ich habe ein &lt;a href="https://blog.schallbert.de/portal-milling/"&gt;Portalfräsmaschinen-Set&lt;/a&gt; gekauft, um die Holzbearbeitung für AnywhereAmps zu delegieren. Ich wollte weniger Zeit für Sägen und Schleifen aufwenden und die Verarbeitungsqualität sowie die Wiederverwendbarkeit verbessern. Aber um die manuelle Holzbearbeitung durch eine mehr oder weniger automatisierte Bearbeitung zu ersetzen, musste ich zunächst meine Pläne in die digitale Welt übertragen.&lt;/p&gt;&#10;&lt;h3 id="computer-aided-design"&gt;Computer Aided Design&lt;/h3&gt;&#10;&lt;p&gt;Ich wählte FreeCAD als Konstruktionswerkzeug, um Zeichnungen zu erstellen. Die Arbeit damit habe ich in einigen &lt;a href="https://blog.schallbert.de/freecad-get-started/"&gt;posts&lt;/a&gt; dokumentiert und meinen Arbeitsablauf z.B. &lt;a href="https://blog.schallbert.de/projects/seifenbutler/#v1-v3-erste-entw%c3%bcrfe-in-hpl"&gt;hier&lt;/a&gt; weiter verbessert. Meine erste Skizze zur Herstellung sah so aus.&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/aa_alpha11/alpha11_cad.jpg" alt="Bild: FreeCAD-Zeichnung Schnappschuss von AnywhereAmps Alpha 1.1" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Mit der Aufteilung der Holzplatte war ich noch nicht ganz zufrieden. Ich fand die Beine zu lang und habe noch einige Stunden in die Verfeinerung des Entwurfs investiert, um weniger Platz zu benötigen, da die Kapazität meiner Portalfräse begrenzt ist.&lt;/p&gt;&#10;&lt;h3 id="computer-aided-manufacturing"&gt;Computer Aided Manufacturing&lt;/h3&gt;&#10;&lt;p&gt;Als ich fertig war, exportierte ich die Skizzen als *.dxf-Dateien (Drawing Interchange File Format (DXF), 1982 von AutoDesk eingeführt. Es wurde schließlich zum Industriestandard für den Austausch von 2D-Zeichnungen) und öffnete sie in meinem CAM-Werkzeug, um Werkzeugwege zu definieren, den XY-Nullpunkt und die Z-Tiefe pro Durchgang festzulegen.&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/aa_alpha11/alpha11_cam.jpg" alt="Bild: CAM-Schnappschuss von AnywhereAmps Alpha 1.1" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h3 id="g-code"&gt;G-Code&lt;/h3&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/aa_alpha11/alpha11_cut.jpg" alt="Image: job01_nesting: all parts cut out" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Am Ende erhält man G-Code-Dateien, die so nachbearbeitet waren, dass sie zur Maschine passten, mit einer geschätzten Bearbeitungszeit von 32 Minuten pro Exemplar. Ich verwendete nur drei Werkzeuge: einen 6-mm-Fräser für die Ausschnitte, einen 2-mm-Fräser für kleinere Löcher und Strukturen und ein Gravierwerkzeug für Fasen und das Logo.&#10;Ich teilte die Arbeit in drei Aufträge auf:&lt;/p&gt;&#10;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha11/finishing.jpg" alt="Bild: job02_finishing: carving the logo" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;code&gt;job00_clamping&lt;/code&gt;, um Löcher und Senkungen zu schneiden, damit das Werkstück am Maschinenbett befestigt werden kann&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;job01_nesting&lt;/code&gt;, um alle Ausschnitte zu fertigen und Gravuren auf dem Verstärkerträger zu machen, aber Anbindungen stehen zu lassen, damit sich die Teile nicht losreißen&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;job02_finishing&lt;/code&gt;, der das Logo auf die (bereits gefärbte) Oberseite graviert und Fasen hinzufügt, wo es nötig ist&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Für die Endbearbeitung musste ich die CAD- und CAM-Dateien aktualisieren, damit sie eine gespiegelte (und um -90° gedrehte) Ansicht des Werkstücks liefern. Dies ermöglicht das zweiseitige Fräsen mit demselben XY-Nullpunkt und den exakt gleichen Seiten, die den Anschlag berühren, um minimale Toleranzen zu erzielen.&lt;/p&gt;&#10;&lt;h3 id="nacharbeit"&gt;Nacharbeit&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/aa_alpha11/waxit.jpg" alt="Bild: Wachsen der Holzoberflächen" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Ich musste noch ein wenig Handarbeit leisten, z. B. Gewindebohrungen abschleifen, Wachs für eine bessere Oberfläche auftragen, und einige Löcher für Streben und Ständer schneiden, die meine CNC nicht fertigen konnte.&lt;/p&gt;&#10;&lt;p&gt;Mit der CNC habe ich Adapter für meine Handoberfräse hergestellt, damit sie über die gebogene Oberfläche des Zylinders gleiten kann. Auf diese Weise konnten die Löcher, die ich zuvor für die Lautsprecher geschnitten hatte, richtig abgeschrägt werden.&lt;/p&gt;&#10;&lt;p&gt;Außerdem ließ ich von der CNC eine Halterung für das Gehäuse im 3D-Modus anfertigen (mit *.STL-Dateien). Auf diese Weise wird der Kessel an seinem Platz gehalten, wenn ich mit meiner Handbohrmaschine Löcher für die Lautsprecher bohre. Übrigens: 3D-Fräsungen brauchen viel Zeit, da die Nachbearbeitung in kleinen Schritten erfolgt, um eine glatte Oberfläche zu erhalten.&lt;/p&gt;&#10;&lt;p&gt;Das Bohren von Löchern mit einem Durchmesser von mehr als &lt;code&gt;100mm&lt;/code&gt; von Hand lässt keine gute Oberflächenqualität auf der Innenseite des Gehäuses zu und die Handhabung des riesigen Bohrers ist immer noch eine Qual. Leider ist meine CNC nicht in der Lage, diese Aufgabe für mich zu übernehmen, daher wird das wohl weiter ein manueller Prozess bleiben.&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/aa_alpha11/shell_adapter.jpg" alt="Bild: Kesselhalter" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h2 id="eindrücke"&gt;Eindrücke&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: AnywhereAmps Alpha V1.1, switched on, with battery attached"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha11/battery.jpg" alt="Image: AnywhereAmps Alpha V1.1, switched on, with battery attached" 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: AnywhereAmps Alpha V1.1, front view"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha11/front_top.jpg" alt="Image: AnywhereAmps Alpha V1.1, front view" 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: AnywhereAmps Alpha V1.1 bottom view"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha11/bottom.jpg" alt="Image: AnywhereAmps Alpha V1.1 bottom view" 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: CNC-cut parts alongside with speaker and shell"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha11/parts.jpg" alt="Image: CNC-cut parts alongside with speaker and shell" class="hugo-gallery__thumb-img" loading="lazy" /&gt;&#10; &lt;/a&gt;&lt;a class="hugo-gallery__thumb" href="#gallery-full-4" aria-label="Image: AnywhereAmps Alpha V1.1, a view inside"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha11/top_open.jpg" alt="Image: AnywhereAmps Alpha V1.1, a view inside" class="hugo-gallery__thumb-img" loading="lazy" /&gt;&#10; &lt;/a&gt;&lt;a class="hugo-gallery__thumb" href="#gallery-full-5" aria-label="Image: Alpha V1.1&amp;#39;s body side view"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha11/side.jpg" alt="Image: Alpha V1.1&amp;#39;s body side view" class="hugo-gallery__thumb-img" loading="lazy" /&gt;&#10; &lt;/a&gt;&lt;/div&gt;&lt;div class="hugo-gallery__caption"&gt;Eindrücke meines AnywhereAmps Alpha V1.1&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/aa_alpha11/battery.jpg" alt="Image: AnywhereAmps Alpha V1.1, switched on, with battery attached" class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__figcaption"&gt;Battery attached and switched on&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/aa_alpha11/front_top.jpg" alt="Image: AnywhereAmps Alpha V1.1, front view" class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__figcaption"&gt;Front view, initial version of speaker protective grille&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/aa_alpha11/bottom.jpg" alt="Image: AnywhereAmps Alpha V1.1 bottom view" class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__figcaption"&gt;A view from below shows the passive radiator, extending the speaker&amp;#39;s low-end&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/aa_alpha11/parts.jpg" alt="Image: CNC-cut parts alongside with speaker and shell" class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__figcaption"&gt;Preparing final assembly&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure id="gallery-full-4" class="hugo-gallery__figure"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha11/top_open.jpg" alt="Image: AnywhereAmps Alpha V1.1, a view inside" class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__figcaption"&gt;A view inside: Ampcarrier and circuits&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure id="gallery-full-5" class="hugo-gallery__figure"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha11/side.jpg" alt="Image: Alpha V1.1&amp;#39;s body side view" class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__figcaption"&gt;Allowing the wax to dry before final assembly&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&#10;&lt;/section&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;Computerized Numerical Control&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;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>AnywhereAmps - Alpha 1.2</title><link>https://blog.schallbert.de/projects/aa_alpha_1_2/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/aa_alpha_1_2/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha11/top-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: AnywhereAmps Alpha 1.2: teaser"&#10; title="AnywhereAmps - Alpha 1.2" /&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Alpha 1.2 v1: 2x15W bi-amped, 2x3.2&amp;quot; Hochmitteltöner, 8&amp;quot; Subwoofer: Sehr gut balancierte, trockene Bässe und gute Höhen, in den Mitten etwas zu kräftig, die höchsten Höhen dafür etwas stumpf. Sehr hohes Gewicht.&lt;/li&gt;&#10;&lt;li&gt;Alpha 1.2 v2: 2x15W bi-amped, 4&amp;quot; PA Treiber, 5.5&amp;quot; passive membrane, 2x piezo tweeters: Sehr ausgeglichener Sound mit guten Lautstärkereserven, dafür ziemlich aufwändig in der Herstellung&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I&amp;rsquo;ll continue writing here once I selected the best variant.&lt;/p&gt;&#10;</description></item><item><title>CarSubAmp</title><link>https://blog.schallbert.de/projects/carsubamp/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/carsubamp/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/carsubamp/carsubamp-thumb.jpg"&#10; class="post-cover"&#10; alt="CarSubAmp"&#10; title="CarSubAmp" /&gt;&#10;&lt;h2 id="projekt-steckbrief"&gt;Projekt-Steckbrief&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Schwierigkeit: Anfänger 1/5&lt;/li&gt;&#10;&lt;li&gt;Kosten: ~50€ ohne Akku&lt;/li&gt;&#10;&lt;li&gt;Zeitaufwand: ~2h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/carsubamp/carsubamp.jpg" alt="carsubamp" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Der CarSubAmp war mein erster Prototyp für einen wirklich tragbaren Basscombo (Lautsprecherbox + Verstärker), der nicht einmal eine Steckdose braucht.&lt;/p&gt;&#10;&lt;h3 id="motivation"&gt;Motivation&lt;/h3&gt;&#10;&lt;p&gt;Während der Covid19-Pandemie habe ich gerne draußen im Park oder auf dem Balkon Bass gespielt und geübt. Das ist kein Problem, wenn man Kopfhörer trägt und einen kleinen Vorverstärker dabei hat. Aber es gab Gelegenheiten, bei denen ich z.B. mit einer Akustikgitarre zusammen spielen wollte, oder jemand brachte eine Synth-App mit einem MIDI-Keyboard mit - und ich war gezwungen, einen Bluetooth-Lautsprecher zu benutzen, der nicht die Ausgangsleistung hatte um akustisch mitzuhalten. Zu allem überfluss verzögert die interne Signalverarbeitung den Sound so sehr, dass sich das Spielen total abgekoppelt anfühlt.&lt;/p&gt;&#10;&lt;h3 id="problem"&gt;Problem&lt;/h3&gt;&#10;&lt;p&gt;Mein richtiger Bassverstärker ist alles andere als mobil. Gewicht: 18kg, Größe: riesig, außerdem: Ohne Steckdose in der Nähe ist er unbrauchbar.&lt;/p&gt;&#10;&lt;h3 id="sackgassen"&gt;Sackgassen&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Anstelle eines Class-D-Verstärkers habe ich den Aux-Eingang eines alten Autoradios verwendet, aber der scheint &amp;ldquo;Line&amp;rdquo;-Pegel erwartet zu haben, den mein Bass nicht liefern konnte. Somit war er kaum hörbar. Außerdem war die Stromaufnahme im Leerlauf &amp;gt;0,5A, sodass die Batterie einfach zu schnell leer war.&lt;/li&gt;&#10;&lt;li&gt;Ich habe die Stromversorgung zunächst mit einer Powerbank (Power-Delivery) ausprobiert, aber der integrierte Aufwärtswandler schien nicht mit Stromstößen bei voller Lautstärke umgehen zu können, so dass die Versorgungsspannung abfiel, was zu hörbaren Verzerrungen führte.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="lösung"&gt;Lösung&lt;/h3&gt;&#10;&lt;p&gt;Nach einigem Grübeln kam ich auf die Idee, einen Li-Ion-Akku für ein kabelloses Multitool zu verwenden, der in der Lage ist, Stromstöße problemlos zu bewältigen, ein geringes Gewicht hat, über eine Akkustandsanzeige und die typischen Schutzschaltungen zur Vermeidung von Überlast, Kurzschluss, Übertemperatur, Tiefentladung usw. verfügt. Ich wollte einen &lt;a href="https://en.wikipedia.org/wiki/Class-D_amplifier" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Class-D&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Leistungsverstärker verwenden, der auf den Spannungsbereich dieser Batterie abgestimmt ist, um möglichst energieeffizient und leicht zu sein&#10;wie möglich.&lt;/p&gt;&#10;&lt;h3 id="der-carsubamp"&gt;Der &amp;ldquo;CarSubAmp&amp;rdquo;&lt;/h3&gt;&#10;&lt;p&gt;Um zu prüfen, ob das Konzept realisierbar war, entnahm ich die Subwoofer-Baugruppe, die normalerweise in der Reserveradmulde sitzt, aus unserem Auto. Ich kaufte eine 20-W-&lt;a href="https://www.adafruit.com/product/1752" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Verstärkerentwicklungsplatine&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und nachdem ich einige Komponenten angelötet, eine Sicherung und eine 6,3-mm-Eingangsbuchse hinzugefügt hatte, war mein erster Prototyp bereit zum Ausprobieren. &lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/carsubamp/carsubamp_explained.jpg" alt="carsubamp components explained" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&lt;/p&gt;&#10;&lt;h2 id="zusammenfassung"&gt;Zusammenfassung&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Sehr leicht, &amp;lt;5kg&lt;/li&gt;&#10;&lt;li&gt;4Ohm Subwoofer-Baugruppe bereits fertig&lt;/li&gt;&#10;&lt;li&gt;belüftetes Design für bessere Lautsprechereffizienz&lt;/li&gt;&#10;&lt;li&gt;Der Klang ist in Ordnung. Die Baugruppe ist allerdings nicht als Vollbereichslautsprecher verwendbar, da die Höhen fehlen.&lt;/li&gt;&#10;&lt;li&gt;Bei tiefen Tönen (d.h. bei oder unter &lt;code&gt;g1&lt;/code&gt;) wird das System merklich leiser&lt;/li&gt;&#10;&lt;li&gt;Entwicklerplatine wird ohne Vorverstärker geliefert, daher ist die maximale Lautstärke bereits etwas niedrig&lt;/li&gt;&#10;&lt;li&gt;Bei hoher Belastung führt die belüftete Konstruktion (Bassreflex) zu hörbaren Flattergeräuschen&lt;/li&gt;&#10;&lt;li&gt;Aufgrund des geringen Durchmessers würde der Lautsprecher bei Hammer-ons auf tiefen Saiten an die Grenzen der Auslenkung stoßen&lt;/li&gt;&#10;&lt;li&gt;Alle Komponenten sind mit Heißkleber oder Klebeband befestigt, daher nicht sehr haltbar&lt;/li&gt;&#10;&lt;li&gt;Der Aufbau ist mit Klebstoff versiegelt. Es gibt keine einfache Möglichkeit, dafür ein Gehäuse zu bauen.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Obwohl diese Konstruktion eindeutige Nachteile hatte, gefiel sie mir sehr gut. Denn zum ersten Mal konnte ich buchstäblich überall und jederzeit spielen, wann ich wollte. Ich erwischte mich sogar auf der Couch mit diesem kleinen Verstärker, obwohl der &amp;ldquo;richtige&amp;rdquo; Verstärker nur einen Raum weiter stand.&lt;/p&gt;&#10;&lt;p&gt;Nach einigen Monaten, in denen ich das Ding mit mir herumtrug, 3 Mal kaputte Verbindungen nachlötete und die Batterie immer wieder neu einklebte, beschloss ich, diesem Projekt &lt;a href="https://blog.schallbert.de/projects/mobfobamp/"&gt;ein Upgrade zu gönnen&lt;/a&gt;.&lt;/p&gt;&#10;</description></item><item><title>ClickEncoder</title><link>https://blog.schallbert.de/projects/encoder/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/encoder/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/encoder/encoder-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Encoder on Breadboard"&#10; title="ClickEncoder" /&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: ~6h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="überblick"&gt;Überblick&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://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;Bibliothek für das &lt;a href="https://www.arduino.cc/en/Reference/Libraries" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Arduino Framework&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, geschrieben in C++.&lt;/p&gt;&#10;&lt;p&gt;Der Quellcode selbst besteht nur aus ein paar hundert Zeilen. Er ist leichtgewichtig und leistungsfähig, da der verbesserte Algorithmus weniger Berechnungen benötigt und gleichzeitig eine bessere Zustandsinterpretation bietet als das ursprüngliche Projekt, von dem sie abgezweigt ist. Die Bibliothek ist offiziell auf &lt;a href="https://platformio.org/lib/show/11808/ClickEncoder" 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; veröffentlicht und wird auf &lt;a href="https://github.com/Schallbert/encoder" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Github&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; gehostet und gepflegt. Sie enthält Beispiele sowohl für Arduino- als auch für PlatformIO-IDEs und bietet Unittests, die nach der Anpassung an die spezifische Anwendung ausgeführt werden können.&lt;/p&gt;&#10;&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;&#10;&lt;p&gt;Ich habe zunächst die von &lt;a href="https://github.com/0xPIT/encoder" target="_blank" rel="noopener noreferrer" class="external-link"&gt;0xPIT&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; veröffentlichte Originalbibliothek verwendet, aber diese verursachte bei mir einige Compiler-Warnungen, außerdem waren die Messwerte des Encoders nicht einwandfrei und manchmal gab es Jitter oder zurückspringende Werte, was ich nicht akzeptieren wollte.&lt;/p&gt;&#10;&lt;h2 id="lösung"&gt;Lösung&lt;/h2&gt;&#10;&lt;p&gt;Da das Archiv ungepflegt aussah und meine Anfrage für Verbesserungen nicht beantwortet wurde, beschloss ich, diese Probleme selbst zu beheben. Ich habe also behutsam einen Teil des Codes umgeschrieben. Für mein damaliges Projekt benötigte ich auch ein &amp;ldquo;Wiederholungssignal&amp;rdquo;, wenn eine Taste kontinuierlich gedrückt wird, also fügte ich auch dies hinzu. Schließlich trennte ich den Encoder-Algorithmus von der Tastenbehandlung und stellte separate Klassen bereit, so dass nun bei Bedarf jede beliebige Kombination aus Taste und Encoder verwendet werden kann, jeweils mit konfigurierbarem Verhalten.&lt;/p&gt;&#10;&lt;h2 id="encoder-algorithmus-erklärt"&gt;Encoder Algorithmus erklärt&lt;/h2&gt;&#10;&lt;h2 id="api"&gt;API&lt;/h2&gt;&#10;&lt;p&gt;Die API von ClickEncoder ist einfach und leicht zu verstehen: &lt;code&gt;service()&lt;/code&gt; führt die Geschäftslogik aus und die Zustandsinterpretation durch. Diese Methode muss regelmäßig ausgeführt werden, um zu überprüfen, ob der Encoder seit seinem letzten Aufruf bewegt wurde, in welche Richtung, wie viele Schritte, und diese Werte dann in einer Akkumulatorvariablen speichern.&#10;&lt;code&gt;getIncrement()&lt;/code&gt; gibt die Schrittänderungen des Encoders seit dem letzten Aufruf zurück. &lt;code&gt;getAccumulate()&lt;/code&gt; gibt die Summe der Inkremente seit dem Start zurück. &lt;code&gt;getButton()&lt;/code&gt; gibt den aktuellen Button-Status zurück.&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:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;service&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;int16_t&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getIncrement&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;int16_t&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getAccumulate&lt;/span&gt;();&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Button&lt;span style="color:#f92672"&gt;::&lt;/span&gt;eButtonStates getButton();&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="encoder-status"&gt;Encoder-Status&lt;/h3&gt;&#10;&lt;p&gt;Der Encoder verwendet einen &lt;a href="https://de.wikipedia.org/wiki/Gray_code" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Gray Code&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zur Kodierung der Schritte. &lt;code&gt;getBitCode()&lt;/code&gt; wandelt diesen Code, der über die Hardware-Pins A und B des Encoders übertragen wird, in eine &amp;ldquo;0&amp;hellip;3&amp;rdquo;-Rasten-Interpretation um, wobei die Zahlen bei einer Rechtsdrehung ansteigen und bei einer Linksdrehung abfallen. Dies geschieht effizient ohne Verwendung von &lt;code&gt;if()&lt;/code&gt; oder anderer Verzweigungslogik.&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:#66d9ef"&gt;uint8_t&lt;/span&gt; Encoder&lt;span style="color:#f92672"&gt;::&lt;/span&gt;getBitCode()&#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;// GrayCode convert&#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;// !A &amp;amp;&amp;amp; !B --&amp;gt; 0&#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;// !A &amp;amp;&amp;amp; B --&amp;gt; 1&#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;// A &amp;amp;&amp;amp; B --&amp;gt; 2&#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;// A &amp;amp;&amp;amp; !B --&amp;gt; 3&#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;uint8_t&lt;/span&gt; currentEncoderRead &lt;span style="color:#f92672"&gt;=&lt;/span&gt; digitalRead(pinA);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; currentEncoderRead &lt;span style="color:#f92672"&gt;|=&lt;/span&gt; (currentEncoderRead &lt;span style="color:#f92672"&gt;&amp;lt;&amp;lt;&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; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// bit0 of result to be inverted if set&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; currentEncoderRead &lt;span style="color:#f92672"&gt;^=&lt;/span&gt; digitalRead(pinB);&#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; currentEncoderRead;&#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;Ich habe hier mit einem Trick gearbeitet, sodass man sich den Code-Schnipsel oben möglicherweise mehrfach ansehen muss. Der Zustand von &lt;code&gt;PinA&lt;/code&gt; wird auf Bit1 von &lt;code&gt;currentEncoderRead&lt;/code&gt; geschrieben, indem ich es mit der &lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt;-Operation nach links schiebe. &lt;em&gt;Gleichzeitig bleibt der Status des 0. Bits durch &lt;code&gt;|=&lt;/code&gt; aber erhalten&lt;/em&gt;. Würde ich jetzt &lt;code&gt;PinB&lt;/code&gt; einfach logisch &lt;code&gt;&amp;amp;=&lt;/code&gt; anbinden, dann würde ich den Gray-Code verletzen, nachdem &lt;code&gt;A&amp;amp;&amp;amp;B=2&lt;/code&gt; sein sollte und nicht &lt;code&gt;3&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Sind &lt;code&gt;A&lt;/code&gt; und &lt;code&gt;B&lt;/code&gt; also beide nicht gesetzt, bleibt das Ergebnis &lt;code&gt;0&lt;/code&gt;, denn &lt;code&gt;00^00=00=0&lt;/code&gt;. Ist nur &lt;code&gt;A&lt;/code&gt; gesetzt, &lt;code&gt;B&lt;/code&gt; aber nicht, so lautet das Ergebnis &lt;code&gt;11^00=11=3&lt;/code&gt;. Wenn &lt;code&gt;B&lt;/code&gt; gesetzt ist, &lt;code&gt;A&lt;/code&gt; aber nicht, ergibt sich &lt;code&gt;00^01=01=1&lt;/code&gt;. Sind beide gesetzt, schließlich &lt;code&gt;11^01=10=2&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Für weitere Informationen zum Thema logische Binäroperationen siehe &lt;a href="https://blog.schallbert.de/projects/fireplace/#galois-lfsr-implementierung"&gt;mein Steckbrett-Lagerfeuer Projekt&lt;/a&gt;&lt;/p&gt;&#10;&lt;h3 id="zustandsinterpretation"&gt;Zustandsinterpretation&lt;/h3&gt;&#10;&lt;p&gt;Nachdem der Bitcode gelesen wurde, wird die Variable &lt;code&gt;rawMovement&lt;/code&gt; gesetzt, die uns sagt, wie weit der Encoder zwischen zwei Durchläufen der &lt;code&gt;service()&lt;/code&gt; Routine gedreht wurde. Hier sieht man, dass dieser Schritt zeitkritisch ist: Es kommt zu Aliasing-Fehlern, wenn der Geber schneller einmal alle Positionen wechselt, als dass &lt;code&gt;service()&lt;/code&gt; erneut aufgerufen wird. In diesem Falle würden Schritte verloren gehen.&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:#66d9ef"&gt;void&lt;/span&gt; Encoder&lt;span style="color:#f92672"&gt;::&lt;/span&gt;handleEncoder()&#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;uint8_t&lt;/span&gt; encoderRead &lt;span style="color:#f92672"&gt;=&lt;/span&gt; getBitCode();&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// bit0 set = status changed, bit1 set = &amp;#34;overflow 3&amp;#34; where it goes 0-&amp;gt;3 or 3-&amp;gt;0&#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;uint8_t&lt;/span&gt; rawMovement &lt;span style="color:#f92672"&gt;=&lt;/span&gt; encoderRead &lt;span style="color:#f92672"&gt;-&lt;/span&gt; lastEncoderRead;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lastEncoderRead &lt;span style="color:#f92672"&gt;=&lt;/span&gt; encoderRead;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// This is the uint-&amp;gt;int magic, converts raw to: -1 counterclockwise, 0 no turn, 1 clockwise&#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;int8_t&lt;/span&gt; signedMovement &lt;span style="color:#f92672"&gt;=&lt;/span&gt; ((rawMovement &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;) &lt;span style="color:#f92672"&gt;-&lt;/span&gt; (rawMovement &lt;span style="color:#f92672"&gt;&amp;amp;&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; encoderAccumulate &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; signedMovement;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; encoderAccumulate &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; handleAcceleration(signedMovement);&#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 ein weiterer Trick: &lt;code&gt;signedMovement&lt;/code&gt; wird berechnet, indem die Bewegung in eine vorzeichenbehaftete Ganzzahl umgewandelt wird, die &lt;code&gt;0&lt;/code&gt; als &amp;ldquo;nicht gedreht&amp;rdquo;, &lt;code&gt;-1&lt;/code&gt; als &amp;ldquo;links gedreht&amp;rdquo; und &lt;code&gt;1&lt;/code&gt; als &amp;ldquo;rechts gedreht&amp;rdquo; darstellt. Dabei wird das Verhalten bei Integer-Überlauf ausgenutzt, in Kombination mit der Interpretation von &amp;ldquo;signedness&amp;rdquo;.&lt;/p&gt;&#10;&lt;p&gt;Abschließend wird die Akkumulation durchgeführt und die Beschleunigung, sofern konfiguriert, dem Akkumulator hinzugefügt, je nachdem, wie schnell der Geber gedreht wird.&lt;/p&gt;&#10;&lt;p&gt;Um diese beiden Methoden ohne Verzweigungen zu programmieren, habe ich mehrere Abende Grübelei und praktisch einen halben Block kariertes Papier benötigt. Offen gesagt haben mich diese paar Zeilen Code an den Rande der Leistungsfähigkeit meines Hirns gebracht. Ich habe das Programmieren eben nicht von der Pike auf gelernt 😅&lt;/p&gt;&#10;&lt;h3 id="die-methode-getaccumulate"&gt;Die Methode getAccumulate()&lt;/h3&gt;&#10;&lt;p&gt;Sie ist wirklich einfach. Sie gibt nur den internen akkumulierten Wert zurück, der der Konfiguration des Encoders entspricht, wie viele Ticks einen Schritt ausmachen.&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:#66d9ef"&gt;int16_t&lt;/span&gt; Encoder&lt;span style="color:#f92672"&gt;::&lt;/span&gt;getAccumulate()&#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; (encoderAccumulate &lt;span style="color:#f92672"&gt;/&lt;/span&gt; stepsPerNotch);&#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;h3 id="die-methode-getincrement"&gt;Die Methode getIncrement()&lt;/h3&gt;&#10;&lt;p&gt;Gibt zurück um wie viele Ticks sich der Encoder seit dem letzten Aufruf dieser Methode bewegt hat.&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:#66d9ef"&gt;int16_t&lt;/span&gt; Encoder&lt;span style="color:#f92672"&gt;::&lt;/span&gt;getIncrement()&#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;int16_t&lt;/span&gt; accu &lt;span style="color:#f92672"&gt;=&lt;/span&gt; getAccumulate();&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;int16_t&lt;/span&gt; encoderIncrements &lt;span style="color:#f92672"&gt;=&lt;/span&gt; accu &lt;span style="color:#f92672"&gt;-&lt;/span&gt; lastEncoderAccumulate;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lastEncoderAccumulate &lt;span style="color:#f92672"&gt;=&lt;/span&gt; accu;&#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; (encoderIncrements);&#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;h2 id="ermittlung-des-tasterzustands"&gt;Ermittlung des Tasterzustands&lt;/h2&gt;&#10;&lt;h3 id="die-methode-handlebutton"&gt;Die Methode handleButton()&lt;/h3&gt;&#10;&lt;p&gt;Alle Tasterzustände leiten sich von zwei Basiszuständen ab: gedrückt oder nicht gedrückt. Dies spiegelt sich in der Logik wider:&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:#66d9ef"&gt;void&lt;/span&gt; Button&lt;span style="color:#f92672"&gt;::&lt;/span&gt;handleButton()&#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; (lastGetButtonCount &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; ENC_BUTTONINTERVAL)&#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;;&#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; lastGetButtonCount &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; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (digitalRead(pinBTN) &lt;span style="color:#f92672"&gt;==&lt;/span&gt; pinActiveState)&#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; handleButtonPressed();&#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;else&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; handleButtonReleased();&#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; (doubleClickTicks &lt;span style="color:#f92672"&gt;&amp;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; &lt;span style="color:#f92672"&gt;--&lt;/span&gt;doubleClickTicks;&#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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Der Taster kann so konfiguriert werden, dass er seltener ausgelesen wird als der Encoder. Wenn dies der Fall ist, bleibt diese Methode zwischenzeitlich ohne Aktion.&lt;/p&gt;&#10;&lt;h3 id="taste-gedrückt"&gt;Taste gedrückt:&lt;/h3&gt;&#10;&lt;p&gt;&lt;code&gt;handleButtonPressed()&lt;/code&gt; kann entweder &lt;code&gt;Closed&lt;/code&gt;, &lt;code&gt;Held&lt;/code&gt; oder &lt;code&gt;LongPressRepeat&lt;/code&gt; zurückgeben, je nachdem wie der Taster konfiguriert ist und wie lange er bereits gedrückt wurde.&lt;/p&gt;&#10;&lt;h3 id="taste-nicht-gedrückt"&gt;Taste nicht gedrückt:&lt;/h3&gt;&#10;&lt;p&gt;&lt;code&gt;handleButtonReleased()&lt;/code&gt; kann entweder &lt;code&gt;Clicked&lt;/code&gt;, &lt;code&gt;Released&lt;/code&gt; oder &lt;code&gt;Doubleclicked&lt;/code&gt; zurückgeben, abhängig von der Konfiguration des Tasters und der Anzahl der Klicks innerhalb einer bestimmten Zeit.&lt;/p&gt;&#10;&lt;h3 id="konfiguration-der-bibliothek"&gt;Konfiguration der Bibliothek&lt;/h3&gt;&#10;&lt;p&gt;Das Verhalten der Encoder/Tastenerkennung kann in der Datei &lt;code&gt;encoder.h&lt;/code&gt; mit Hilfe der unten aufgeführten Konstanten geändert werden. Die Werte wurden bereits optimiert und fühlen sich, zumindest für mich, natürlich an. Sie sollten geändert werden, wenn Sie ein anderes Serviceintervall als &lt;code&gt;1ms&lt;/code&gt; verwenden möchten, da diese Werte alle dieses Intervall als Referenz nehmen.&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;// ----------------------------------------------------------------------------&#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;// Acceleration configuration (for 1ms calls to ::service())&#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;//&#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;constexpr&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; ENC_ACCEL_START &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;150&lt;/span&gt;; &lt;span style="color:#75715e"&gt;// The smaller this value, the quicker you must turn to activate acceleration.&#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;constexpr&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; ENC_ACCEL_SLOPE &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;75&lt;/span&gt;; &lt;span style="color:#75715e"&gt;// the smaller this value, the stronger the acceleration will manipulate values.&#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;// Button configuration (values for 1ms timer service calls)&#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;//&#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;constexpr&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; ENC_BUTTONINTERVAL &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;; &lt;span style="color:#75715e"&gt;// check button every x ms, also debouce time&#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;constexpr&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;uint16_t&lt;/span&gt; ENC_DOUBLECLICKTIME &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;400&lt;/span&gt;; &lt;span style="color:#75715e"&gt;// second click within x ms&#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;constexpr&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;uint16_t&lt;/span&gt; ENC_LONGPRESSREPEATINTERVAL &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;200&lt;/span&gt;; &lt;span style="color:#75715e"&gt;// reports repeating-held every x ms&#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;constexpr&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;uint16_t&lt;/span&gt; ENC_HOLDTIME &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1200&lt;/span&gt;; &lt;span style="color:#75715e"&gt;// report held button after x ms&#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;// ----------------------------------------------------------------------------&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="verwenden-sie-sie-in-ihrem-projekt"&gt;Verwenden Sie sie in Ihrem Projekt!&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Die Bibliothek ist eine Open-Source-Software, die unter der MIT-Lizenz steht.&lt;/li&gt;&#10;&lt;li&gt;PlatformIO-Benutzer? Ganz einfach, suchen Sie sie in der Ansicht &amp;lsquo;Bibliotheken&amp;rsquo;, die Tags sind Arduino, Encoder, Schallbert. Dann ist es nur noch ein Klick, um sie zu Ihrer Lösung hinzuzufügen.&lt;/li&gt;&#10;&lt;li&gt;Alternativ können Sie auch das &lt;a href="https://github.com/Schallbert/encoder" 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; klonen oder forken und die Header/Cpp-Datei nach eigenem Ermessen verwenden.&lt;/li&gt;&#10;&lt;li&gt;Sie möchten etwas beitragen? Vielleicht haben Sie eine effizientere Zustandsberechnung für die Taster? Ich würde mich freuen, Ihren Pull Request in meinem Posteingang zu finden.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item><item><title>CNC 'Opferplatte'</title><link>https://blog.schallbert.de/projects/cnc_spoilboard/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/cnc_spoilboard/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/spoilboard/spoilboard-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Spoilboard for my CNC"&#10; title="CNC &amp;#39;Opferplatte&amp;#39;" /&gt;&#10;&lt;h2 id="was-ist-eine-opferplatte"&gt;Was ist eine &amp;lsquo;Opferplatte&amp;rsquo;?&lt;/h2&gt;&#10;&lt;p&gt;Eine &amp;lsquo;Opferplatte&amp;rsquo; ist ein Brett, was auf dem Maschinenbett befestigt und zumeist abgeplant wird. Auf ihm wiederum werden dann die Werkstücke befestigt. Es nennt sich Opferplatte, weil die Fräser beim Durchfräsen des Werkstückes meist einige Zehntel Millimeter tiefer in das Material tauchen als es stark ist, um jegliche Fasern sicher zu trennen und die Werkstückunterseite nicht auszufransen.&lt;/p&gt;&#10;&lt;p&gt;Zudem können Werkstücke ohne viel Aufhebens direkt auf der Platte festgeschraubt werden. Ist die Platte einige Zeit in betrieb und droht, uneben zu werden, lässt sie sich meist noch ein paar weitere male abplanen und somit weiternutzen, bevor sie dann nach langem produktiven Einsatz erneuert werden muss.&lt;/p&gt;&#10;&lt;p&gt;Ein weiterer Vorteil der Opferplatte ist, dass selbst bei fehlerhaften Fräsbefehlen nicht direkt der teure Maschinentisch in Mitleidenschaft gezogen wird und zumeist sogar der Fräser diese Fehler überlebt.&lt;/p&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: ~60€&lt;/li&gt;&#10;&lt;li&gt;Zeitaufwand: ~6h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;a href="https://blog.schallbert.de/cnc-spoilboard-diy/"&gt;Zur Anleitung&lt;/a&gt;&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/spoilboard/crop.jpg" alt="Image: spoilboard features" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Das Design verwendet Schlitze für Hammermuttern in verschiedenen Winkeln, um eine sehr flexible Aufspannung zu ermöglichen. Es umfasst auch einen Anschlag, der die Vorbereitung bei Fertigung aus mehreren Plattenrohlingen erleichtert: Der Werkstücknullpunkt liegt immer an der gleichen Stelle, und das Einspannen gegen den Anschlag genügt meist bereits, so dass kein Niederhalten in Z-Richtung erforderlich ist. Der Anschlag hat zwei Aussparungen: Eine zur Aufnahme des Werkzeuglängensensors und eine weitere am XY-Nullpunkt, um Werkzeugkollisionen beim schnellen Positionieren zu vermeiden.&lt;/p&gt;&#10;&lt;h3 id="einfache-einstellung-für-fast-jede-cnc"&gt;Einfache Einstellung für fast jede CNC&lt;/h3&gt;&#10;&lt;p&gt;Die Maße, die ich genommen habe, passen für eine Basicline 0607 Portalfräsmaschine. Da ich aber ein parametrisches CAD-Tool verwende, müssen nur die Abmessungen an Ihre Maschine angepasst und einige Zeichnungsfehler behoben werden, sodass eigene Entwürfe schnell von der Hand gehen sollten.&lt;/p&gt;&#10;&lt;h2 id="die-idee"&gt;Die Idee&lt;/h2&gt;&#10;&lt;p&gt;Wie bereits erwähnt, muss meine Opferplatte die folgenden Kriterien erfüllen:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Sie muss auf dem Grundrahmen der CNC-Maschine befestigt werden können (Hammermuttern / M5-Schrauben)&lt;/li&gt;&#10;&lt;li&gt;Befestigungspunkt in der Nähe des XY-Nullpunkts&lt;/li&gt;&#10;&lt;li&gt;T-Nuten für flexible Montage bei 0°, 22,5°, 45°, 67,5°, 90°&lt;/li&gt;&#10;&lt;li&gt;Anschlag, Schnellmontage auf der Opferplatte zur Vereinfachung des Spannens und Verbesserung der Wiederholgenauigkeit&lt;/li&gt;&#10;&lt;li&gt;Anschlag zur Aufnahme des Werkzeuglängensensors&lt;/li&gt;&#10;&lt;li&gt;Anschlag zur Bereitstellung eines Ausschnitts am XY-Nullpunkt&lt;/li&gt;&#10;&lt;li&gt;Soll auf der CNC, für die sie entwickelt wurde, herstellbar sein (bei Bedarf mehrstufig)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="entwurfsphase-cad"&gt;Entwurfsphase: CAD&lt;/h2&gt;&#10;&lt;p&gt;Ich begann mit einer Maßtabelle in Freecad und ein paar leeren Skizzen.&lt;/p&gt;&#10;&lt;h3 id="montage-am-grundrahmen"&gt;Montage am Grundrahmen&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/spoilboard/spoilboard_mount.jpg" alt="Image: Opferplatte Draufsicht, Montage am Rahmen" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;In die Tabellenkalkulation habe ich zunächst die Abmessungen des Grundrahmens und des CNC-Verfahrbereichs sowie die Parameter der Einschlagmutter, der Unterlegscheibe und des Befestigungselements eingegeben.&#10;Ich maß die genauen Positionen der T-Muttern des Grundrahmens, an denen ich das Opferplatte befestigen wollte, und gab sie in die Kalkulationstabelle ein.&lt;/p&gt;&#10;&lt;p&gt;Danach öffnete ich die erste Skizze und zeichnete das Opferplatten-Rechteck, wobei ich die Abmessungen mit den Daten aus der Tabelle so hinzufügte, dass die Skizze &amp;ldquo;fully constrained&amp;rdquo; - also vollständig bestimmt ist. Ich fügte Konstruktionslinien hinzu (d. h. Linien, die in der endgültigen Zeichnung nicht zu sehen sind, aber dazu dienen, Zwangsbedingungen zu verankern, Linien zu zeichnen oder die Symmetrie innerhalb einer Zeichnung zu markieren), wo die Zentren der T-Muttern des Grundrahmens positioniert sind.&lt;/p&gt;&#10;&lt;p&gt;Weiter fügte ich konzentrische Kreise für Unterlegscheibensenkungen und Durchgangslöcher in gleichmäßigen Abständen hinzu. Sie werden später die Befestigungselemente und ihre Unterlegscheiben aufnehmen. Über ihre endgültige Position auf der Y-Achse habe ich noch nicht entschieden, da die T-Muttern der Opferplatte einige dieser Befestigungspunkte beeinträchtigen würden.&lt;/p&gt;&#10;&lt;p&gt;Die CNC-Bearbeitung für diese Skizze muss auf der &amp;ldquo;Oberseite&amp;rdquo; der Opferplatte durchgeführt werden.&lt;/p&gt;&#10;&lt;h3 id="schlitze-für-t-muttern-und-befestigungspunkte-für-den-anschlag"&gt;Schlitze für T-Muttern und Befestigungspunkte für den Anschlag&lt;/h3&gt;&#10;&lt;p&gt;Nun war es wieder an der Zeit, der Tabelle einige Daten hinzuzufügen: T-Nut-Parameter, Winkel, Randbreite wie folgt:&#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/spoilboard/tslot_data.jpg" alt="Image: spoilboard t-nut slot data" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Zunächst musste ich auf dem Opferplatte Konstruktionslinien hinzufügen, um den Verfahrbereich der CNC zu markieren. Danach musste ich etwas Platz für den Anschlag reservieren. Erst dann konnte ich mit dem Zeichnen und Positionieren der T-Nuten beginnen, die jeweils unterschiedliche Winkel und Längen hatten, je nachdem, welchen Rand ich aus Stabilitätsgründen beibehalten wollte.&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/spoilboard/spoilboard_drawing.jpg" alt="Image: spoilboard t-nut slot drawing" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Jede T-Nut erhielt eine Symmetrie-Konstruktionslinie, um die herum zwei Rechtecke zur Markierung der Schlitz- und Falzschultern liegen.&lt;/p&gt;&#10;&lt;p&gt;Dann fügte ich Senkungen für Schraubmuttern hinzu, die später den Anschlag am Rand der Opferplatte halten werden. In ihrer Mitte sind Durchgangsbohrungen für die Befestigungselemente vorgesehen.&lt;/p&gt;&#10;&lt;p&gt;In einem letzten Schritt passte ich die Positionen der Löcher in der Skizze für die Grundrahmenbefestigung an, wo sie mit den Schlitzen für die T-Muttern kollidierten.&lt;/p&gt;&#10;&lt;p&gt;Die CNC-Arbeit für diese Skizze wird auf der &amp;ldquo;Unterseite&amp;rdquo; der Opferplatte ausgeführt - die Hammermuttern funktionieren nur, wenn sie von unten greifen.&lt;/p&gt;&#10;&lt;h3 id="anschlag"&gt;Anschlag&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/spoilboard/fence.jpg" alt="Image: spoilboard fence" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Hier müssen nur noch einige weitere Daten in das Tabellenblatt eingefügt werden: Ellipsoidabmessungen für den XY-Nullpunktausschnitt, den Werkzeuglängensensor und die Breite des Anschlags auf der Y-Achse. Seine Abmessungen auf der X-Achse waren bereits durch den Verfahrbereich der Maschine vorgegeben.&lt;/p&gt;&#10;&lt;p&gt;Das Zeichnen des Anschlags war einfach, da ich mit dem Werkzeug &amp;ldquo;Kante verbinden&amp;rdquo; Positionen von Linien aus anderen Skizzen importieren konnte. Der Anschlag ist ein eigener &amp;ldquo;Job&amp;rdquo; mit einem eigenen physischen Teil auf der CNC, und er wird nur auf der Oberseite gefräst.&lt;/p&gt;&#10;&lt;h2 id="planungsphase-cam"&gt;Planungsphase: CAM&lt;/h2&gt;&#10;&lt;p&gt;Jetzt kam der schwierige Teil: Statt drei Jobs wie oben angegeben, brauchte ich eigentlich sechs. Das liegt daran, dass der Verfahrbereich der CNC kleiner ist als die Abmessungen der Opferplatte. Außerdem plante ich mit Rohlingen, die bereits die richtigen Umrissmaße hatten, damit ich sie auf die Maschine einpassen konnte.&lt;/p&gt;&#10;&lt;p&gt;Der erste Teil dieser Arbeit bestand darin, Senkungen und Löcher für das untere Ende der Opferplatten-Halterungen zu schaffen. Ich setzte den Nullpunkt ganz unten links auf der Maschine (X0, Y-max), damit ich sicher sein konnte, dass die relativen Koordinaten passen würden.&#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/spoilboard/mount_cam.jpg" alt="Image: spoilboard cam mount_lower" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&lt;/p&gt;&#10;&lt;p&gt;Für den oberen Abschnitt wurde das oberste Loch auf der linken Seite als XY-Referenznullpunkt verwendet, der in der vorherigen Arbeit erstellt wurde. Auf diese Weise konnte ich das Material flexibler einspannen.&#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/spoilboard/mount_cam_up.jpg" alt="Image: spoilboard cam mount_upper" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&lt;/p&gt;&#10;&lt;p&gt;Ich wiederholte diese Vorgehensweise für die beiden anderen Jobs.&lt;/p&gt;&#10;&lt;p&gt;Für die T-Nut Schlitze musste ich die Skizze spiegeln und mein Material um die Y-Achse drehen, da die Unterseite der Opferplatte hier bearbeitet werden musste. Das könnte man auch in CAM machen, aber ich zog es vor, für diese Aufgabe schnell wieder ins CAD zu wechseln.&#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/spoilboard/tslot_cam.jpg" alt="Image: spoilboard cam tslot_lower" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&lt;/p&gt;&#10;&lt;p&gt;Für all diese Arbeiten habe ich einen 6-mm-Holzfräser mit 2 Schneiden und einer Fräserlänge von &lt;code&gt;21mm&lt;/code&gt; verwendet. Da dies meine allerersten Schritte mit einer CNC waren, habe ich mit sehr konservativen Parametern für dieses Werkzeug geplant: &lt;code&gt;S24000U/min&lt;/code&gt;, &lt;code&gt;F2800mm/min&lt;/code&gt;, &lt;code&gt;Z+=6mm&lt;/code&gt;, &lt;code&gt;XY+=85%&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Ich habe den ersten Job sogar &amp;ldquo;in der Luft&amp;rdquo; ausgeführt, um sicherzugehen, dass ich alles richtig eingestellt hatte.&lt;/p&gt;&#10;&lt;h2 id="ausführung"&gt;Ausführung&lt;/h2&gt;&#10;&lt;p&gt;Ich war ratlos, als ich das Material endlich auf dem Maschinenbett hatte: Wo sollte ich das bloß einspannen? Die Seiten waren durch das Portal belegt, was sich ja bewegen würde. An der Rückseite der Maschine ist bereits die Kellerwand&amp;hellip; Würde es also ausreichen, das Werkstück nur an der Vorderseite einzuspannen? Und da ich ganz durchfräsen wollte, sollte ich eine weitere Opferplatte unter meiner Opferplatte anbringen, damit ich nicht versehentlich in das Maschinenbett schnitt?&lt;/p&gt;&#10;&lt;p&gt;Ich beschloss, dass diese Einwände berechtigt seien, und wartete, bis das Wochenende vorbei war, um etwas Altholz und mehr doppelseitiges Klebeband zu kaufen.&lt;/p&gt;&#10;&lt;p&gt;So habe ich niedergehalten: Standard-Zwingen unten, doppelseitiges Klebeband oben. Hat sehr gut funktioniert.&#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/spoilboard/tslot.jpg" alt="Image spoilboard cutting executed" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&lt;/p&gt;&#10;&lt;p&gt;Bevor ich den Rahmen endgültig auf der Opferplatte befestigte, habe ich ihn mit einer &amp;ldquo;Spiral&amp;rdquo;-Strategie abgeplant. Es genügte bereits &lt;code&gt;Z=0,3mm&lt;/code&gt; tief zu fräsen. Dabei kam ein 14-mm-Planfräser bei einer Geschwindigkeit von nur &lt;code&gt;5500U/min&lt;/code&gt; zum Einsatz. Fast 30 Minuten vergingen während der Ausführung. Das Ergebnis war eine Oberfläche, die so glatt war wie die des Originalmaterials.&#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/spoilboard/leveling.jpg" alt="Image: spoilboard leveling" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&lt;/p&gt;&#10;&lt;h2 id="bewertung"&gt;Bewertung&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/spoilboard/hole.jpg" alt="IMage: Very clean cut in MDF" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Zum Glück hat am Ende alles geklappt. Wie sauber der die Schnitte sind! MDF und scharfe Schaftfräser scheinen gut miteinander zu harmonieren. Hinweis: Für Arbeiten in MDF braucht man eine wirklich gute Absaugung, da bei der Bearbeitung atemberaubende Mengen an Staub entstehen.&lt;/p&gt;&#10;&lt;p&gt;Mit der Gesamtqualität bin ich sehr zufrieden. Jeder einzelne Befestigungspunkt an Maschinenbett und Anschlag passt, und der Werkzeuglängensensor sitzt gut in seiner Aussparung.&lt;/p&gt;&#10;&lt;p&gt;Es gibt zwei Dinge zu bemängeln: Der von mir verwendete Schaftfräser ist sehr laut und gibt ein unangenehmes kreischendes Geräusch von sich, dem ich sogar &lt;a href="https://blog.schallbert.de/my-endmill-screams/"&gt;einen Blogbeitrag gewidmet habe&lt;/a&gt;.&#10;Zweitens habe ich mich in meinem CAM-Tool nicht ausreichend um die Fräsrichtung gekümmert. Auf diese Weise entschied sich das CAM-Tool, einen Durchgang im Gegenlauf und einen weiteren im Gleichlauf durchzuführen (der Schlitz war so breit wie das Werkzeug, für das CAM-Tool ist es also beides Gleichlauffräsen, nur die &amp;ldquo;Seite&amp;rdquo; des Werkzeugeingriffs ändert sich). Der Unterschied ist ein sehr kleiner, aber spürbarer Grat an der Kante des Anschlags.&lt;/p&gt;&#10;&lt;p&gt;Mit dieser Opferplatte kann ich jetzt Platten bis zu einer Größe von 620 mm x 620 mm bearbeiten!&#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/spoilboard/completed.jpg" alt="Image: spoilboard jobs complete" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&lt;/p&gt;&#10;&lt;p&gt;Nun, ein Manko ist noch der Werkzeuglängensensor. Er ragt in den Arbeitsbereich hinein, was ein Problem darstellt, wenn ich große Werkstücke bearbeiten will und Werkzeugwechsel geplant sind: In diesem Fall verwende ich Makros zur Messung der Werkzeuglänge, weshalb ich den Sensor nicht entfernen möchte. Als Iteration könnte ich den Anschlag so überarbeiten, dass er den Sensor vollständig umschließt.&lt;/p&gt;&#10;&lt;p&gt;Vielleicht.&lt;/p&gt;&#10;&lt;p&gt;Später.&lt;/p&gt;&#10;</description></item><item><title>Diese Website programmieren</title><link>https://blog.schallbert.de/projects/thissite/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/thissite/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/thissite/desktop-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Schallbert&amp;#39;s Desktop"&#10; title="Diese Website programmieren" /&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: ~15h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="die-idee"&gt;Die Idee&lt;/h2&gt;&#10;&lt;p&gt;Die Idee entstand durch mein &lt;a href="https://blog.schallbert.de/projects/mobfobamp/"&gt;MobFobAmp&lt;/a&gt;-Projekt, für das ich kein Github-Repository hatte, da es keinen Code oder ähnliche Dokumentation gab. Also beschloss ich eine Website einzurichten, auf der ich Anleitungen für dieses Open-Hardware-Projekt bereitstellen kann, damit andere das Projekt nachbauen können. Ich wollte eine einfach zu hostende Lösung, die nicht zu viel Arbeit beim Einrichten erfordert. Da ich keinen eigenen Server habe, habe ich um Hilfe gebeten. &lt;a href="https://github.com/moritzmar" target="_blank" rel="noopener noreferrer" class="external-link"&gt;moritzmar&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; empfahl mir, für den Anfang &amp;ldquo;Github-Pages&amp;rdquo; als Site-Host zu verwenden, um eine einfache automatische Bereitstellung und globale Reichweite zu gewährleisten. Also habe ich das mal ausprobiert.&lt;/p&gt;&#10;&lt;h2 id="erste-schritte"&gt;Erste Schritte&lt;/h2&gt;&#10;&lt;p&gt;Eine statische Site kam mir genau Recht. Ohne die Notwendigkeit einer Datenbank oder eines großen Content-Management-Frameworks bleibt mein Site-Layout einfach und minimalistisch; ich wollte am Anfang nur dieses eine Projekt hosten.&#10;&lt;a href="https://docs.github.com/en/pages/setting-up-a-github-pages-site-with-jekyll/about-github-pages-and-jekyll" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Github empfiehlt die Verwendung von Jekyll&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://upload.wikimedia.org/wikipedia/commons/4/42/Jekyll_%28software%29_Logo.png" alt="Image: Jekyll Logo" /&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Jekyll Logo&lt;/span&gt;&lt;a href="https://commons.wikimedia.org/wiki/File:Jekyll_%28software%29_Logo.png#file" class="attr-link" aria-label="Attribution 1"&gt;&#10; &lt;sup class="attr-id"&gt;[1]&lt;/sup&gt;&#10; &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&lt;/p&gt;&#10;&lt;p&gt;als Website-Generator. Ich hatte einige Probleme mit dem Windows Subsystem für Linux (WSL), da es meine Terminaleingaben nicht akzeptierte, also folgte ich der &lt;a href="https://jekyllrb.com/docs/installation/windows/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Anleitung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und verwendete den RubyInstaller, der bei mir sofort funktionierte.&lt;/p&gt;&#10;&lt;h2 id="einrichtung-der-site"&gt;Einrichtung der Site&lt;/h2&gt;&#10;&lt;p&gt;Mir wurde klar, dass ich vielleicht mehrere meiner Projekte dokumentieren möchte. Deshalb kam ich auf die Idee, Projekte in Gruppen zusammenzufassen, jede mit einer Überschrift, einem kleinen Text und einem Miniaturbild. Wenn man darauf klickt, kommt man auf die jeweilige Projektseite. Das würde eine Menge Programmierarbeit für die Seiteneinrichtung und das Layout bedeuten, mit der ich nicht allzu viel Zeit verbringen wollte - meine anderen Projekte warten&amp;hellip;&lt;/p&gt;&#10;&lt;h2 id="erster-versuch-mit-themes"&gt;Erster Versuch mit Themes&lt;/h2&gt;&#10;&lt;p&gt;Also folgte ich einer weiteren Anregung von Github-Pages: Die Verwendung von vordefinierten &amp;ldquo;Themes&amp;rdquo;.&#10;Zuerst habe ich das &lt;a href="http://jekyllthemes.org/themes/agency/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Agency Theme&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; verwendet. Es hat eine tolle Landing Page mit einem großen Bild und unterstützt Sammlungen, die das von mir gewünschte Gruppieren von Projekten ermöglichen. Leider ist das Thema ein &amp;ldquo;Remote-Thema&amp;rdquo;, so dass die Anpassungsmöglichkeiten begrenzt waren, z. B. konnte ich eine Sammlung nicht auf eine andere Seite verweisen lassen, sondern es öffnete sich immer ein &amp;ldquo;Popup&amp;rdquo;, und das wollte ich einfach nicht. (Später habe ich herausgefunden, dass Remote-Layouts überschrieben werden können, aber da war es zu spät, denn ich hatte bereits zu einem anderen Theme-Anbieter gewechselt&amp;hellip;)&#10;Das Theme, das ich derzeit verwende, ist das &amp;ldquo;Minimal Mistakes Theme&amp;rdquo; (siehe Fußzeile der Website), das sehr gut dokumentiert ist. Nach dem Herunterladen und ein paar ersten Schritten kämpfte ich mit benutzerdefinierten Farben (_sass) und den Kollektionen - wieder einmal - da ich zunächst nicht verstand, wie Jekyll die Bindungen hinter den Kulissen durchführt. Einige meiner Schwierigkeiten sind in den Blog-Beiträgen vom Juni 2021 zu finden.&lt;/p&gt;&#10;&lt;h3 id="seitenlayouts-hero-screens"&gt;Seitenlayouts: Hero Screens&lt;/h3&gt;&#10;&lt;p&gt;Um die Navigation einfach und intuitiv zu gestalten, sollte man sich ein paar Gedanken darüber machen, wie das Layout der Seite aussehen soll. Die Herangehensweise wird sich bei jedem Seitentyp aufgrund der unterschiedlichen Komplexität und Dokumentstruktur unterscheiden. &lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/thissite/hero_landing.jpg" alt="Image: landing hero" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Auf der Landing-/Homepage zum Beispiel soll die Navigation mitlaufen, so dass die Kategorien der Website immer sichtbar sind. Sie soll ein großes, breites Overlay-Bild mit der Seitenbeschreibung erhalten. Darunter sollen die neuesten Beiträge aufgelistet werden.&#10;Dies ist das Beispiel für die &amp;ldquo;Hero Pages&amp;rdquo; der Kollektion: &lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/thissite/hero_collection.jpg" alt="Image: collection hero" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Die Projekte und ihre Inhalte enthalten eine Seitennavigation in der linken Seitenleiste, können ein scrollbares Panoramabild enthalten und haben eine breite Einstellung, um eine große Menge an Text im Inhaltsbereich anzuzeigen.&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/thissite/hero_project.jpg" alt="Image: project hero" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Die &amp;ldquo;hero screen&amp;rdquo;-Skizzen für die Layouts dieser Website finden Sie &lt;a href="https://blog.schallbert.de/assets/docs/site_hero_screens.pdf"&gt;hier&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="sammlungen--kollektionen"&gt;Sammlungen / Kollektionen&lt;/h3&gt;&#10;&lt;p&gt;Sammlungen (englisch &amp;ldquo;Collections&amp;rdquo;) sind eine gute Möglichkeit, Inhalte zu gruppieren. Sehen Sie sich meine &lt;a href="https://blog.schallbert.de/projects/"&gt;Open Hardware Projekte&lt;/a&gt; Seite an. Sie können alle Projekte in dieser Kategorie mit einem kleinen Teaser-Bild und einer Beschreibung finden, um einen Überblick zu bekommen.&lt;/p&gt;&#10;&lt;p&gt;So kann die Konfiguration der Sammlung aussehen:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Überlegen Sie sich einen passenden Namen für Ihre Sammlung, z.B. &lt;code&gt;electronics&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Der Name der Sammlung wird in &lt;code&gt;_config.yml&lt;/code&gt; als &lt;code&gt;collections: electronics&lt;/code&gt; definiert.&lt;/li&gt;&#10;&lt;li&gt;Der Sammlungsname muss in &lt;code&gt;#defaults&lt;/code&gt; als &lt;code&gt;type: electronics&lt;/code&gt; gesetzt werden, damit Jekyll weiß, wie der tatsächliche Inhalt zu interpretieren ist.&lt;/li&gt;&#10;&lt;li&gt;Eine Sammlungsseite muss erstellt werden (z.B. im Ordner &lt;code&gt;_pages&lt;/code&gt;) mit dem Attribut &lt;code&gt;collection: electronics&lt;/code&gt;. Dies ist die Seite, die die Übersicht der Artikel in der Sammlung anzeigt.&lt;/li&gt;&#10;&lt;li&gt;Es muss ein Ordner mit demselben Namen wie der Name der Sammlung mit führendem Unterstrich erstellt werden: &lt;code&gt;_electronics&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Alle Dokumente in diesem Ordner werden nach einem vollständigen Bauvorgang automatisch auf der Sammlungsseite angezeigt.&lt;/li&gt;&#10;&lt;li&gt;Diese Dokumente sollten ein &amp;ldquo;front matter&amp;rdquo; (Abschnitt vor dem eigentlichen Inhalt in der Datei, gekennzeichnet mit &lt;code&gt;---&lt;/code&gt;) erhalten, damit sie wie in der entsprechenden &lt;code&gt;_layouts&lt;/code&gt;-Datei beschrieben angezeigt werden können, z.B. &lt;code&gt;collection&lt;/code&gt;. Vergisst man dies, wirft einem Jekyll in der Konsole seitenlange, unverständliche Fehlermeldungen um die Ohren.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="mehr-als-das-standard-theme"&gt;Mehr als das Standard-Theme&lt;/h2&gt;&#10;&lt;p&gt;Ich habe zwar einige Theme-Standardeinstellungen überschrieben, verwende aber zumeist die bestehendenTheme-Infrastruktur. Dennoch hat meine Seite ein paar spezielle Anforderungen, so dass ich das Theme anpassen und neue Funktionen hinzufügen musste. Diese werden in den folgenden Abschnitten beschrieben.&lt;/p&gt;&#10;&lt;h3 id="navigation"&gt;Seitenleiste: Seitennavigation&lt;/h3&gt;&#10;&lt;p&gt;Da der Navigationsbaum meiner Website ziemlich flach ist und ich nicht viele Seiten habe, zu denen ich navigieren muss, wollte ich, dass die linke Seitenleiste die Seitennavigation und nicht die Website-Navigation enthält. Daher bräuchte ich keine rechte Seitenleiste, so dass mehr Platz für den Seiteninhalt zur Verfügung stünde.&lt;/p&gt;&#10;&lt;h3 id="inhaltsverzeichnis-auf-der-linken-seite"&gt;Inhaltsverzeichnis auf der linken Seite&lt;/h3&gt;&#10;&lt;p&gt;Wie &lt;a href="https://blog.schallbert.de/jekyll-toc/"&gt;hier&lt;/a&gt; und &lt;a href="https://blog.schallbert.de/jekyll-toc/"&gt;dort&lt;/a&gt; geschrieben, war ich nie 100%ig zufrieden damit, wie meine Projektseiten aussahen: &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/2021-06-17_before_tocleft.jpg" alt="Image: Projektseite bevor ich toc_left einrichtete" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&lt;/p&gt;&#10;&lt;p&gt;Die ursprüngliche Idee des Themes ist, dass der Inhalt von zwei Seitenleisten eingerahmt wird. Die linke kann entweder Autoreninformationen, benutzerdefinierte Inhalte oder die Seitennavigation anzeigen. Der Inhaltsbereich ist zwischen breit und normal konfigurierbar, wobei in letzterem Fall Platz für eine zweite Seitenleiste auf der rechten Seite bleibt. Die rechte Seitenleiste ist so konfigurierbar, dass sie entweder gar nicht vorhanden ist oder das Inhaltsverzeichnis der Seite anzeigt.&lt;/p&gt;&#10;&lt;p&gt;Das Ziel war also die rechte Seitenleiste für mein Design zu eliminieren und statt der Seitennavigation in der linken Seitenleiste die Seitennavigation dorthin zu verschieben.&lt;/p&gt;&#10;&lt;h3 id="sidebar"&gt;Wo das Design der Seitenleiste definiert wird&lt;/h3&gt;&#10;&lt;p&gt;Es geht um den Ordner &lt;code&gt;/_layouts&lt;/code&gt;, in dem Informationen darüber gespeichert werden, wie ein Seitenlayout aussehen soll. Das fragliche Seitendesign heißt &lt;code&gt;single.html&lt;/code&gt; und ruft an einer bestimmten Stelle&#10;&lt;code&gt;include sidebar.html&lt;/code&gt; auf, das sich in &lt;code&gt;/_includes/sidebar.html&lt;/code&gt; befindet. Also habe ich mir einen neuen Parameter ausgedacht, mit dem ich die Anzeige des Inhaltsverzeichnisses in der linken Seitenleiste steuern kann, habe &lt;code&gt;toc_left&lt;/code&gt; gewählt und eine Bedingung hinzugefü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-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;sidebar sticky&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;lt;!-- ...&#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;{.% if page.toc_left %}&#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 toc_left nav=page.toc_left %}&#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;{.% endif %}&#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;... --&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;(Trick: Ich musste &lt;a href="https://en.wikipedia.org/wiki/Jinja_%28template_engine%29" target="_blank" rel="noopener noreferrer" class="external-link"&gt;jinja&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; code auskommentieren und mit &lt;code&gt;...&lt;/code&gt; anführen, denn &lt;a href="https://jekyllrb.com/docs/liquid/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Liquid&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; würde sie sonst selbst als Kommandos erkennen)&lt;/p&gt;&#10;&lt;h3 id="implementierung-des-inhaltsverzeichnisses"&gt;Implementierung des Inhaltsverzeichnisses&lt;/h3&gt;&#10;&lt;p&gt;Es sollte in der linken Seitenleiste erscheinen. Also muste ich nur eine Datei namens &lt;strong&gt;toc_left&lt;/strong&gt; erzeugen, die den Generator für das Inhaltsverzeichnis wie folgt aufruft:&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-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;aside&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;toc_left&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;nav&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;toc&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;markdown&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;1&amp;#34;&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;header&lt;/span&gt;&amp;gt;&amp;lt;&lt;span style="color:#f92672"&gt;h4&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;nav__title&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;&lt;span style="color:#f92672"&gt;i&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;fas fa-{{ include.icon | default: &amp;#39;file-alt&amp;#39; }}&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style="color:#f92672"&gt;i&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;lt;!-- ...&#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.title | .default: site.data.ui-text[site.locale].toc_label }}&amp;lt;/h4&amp;gt;&amp;lt;/header&amp;gt;&#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 toc.html sanitize=true html=content h_min=1 h_max=6 class=&amp;#34;toc__menu&amp;#34; skip_no_ids=true %} &#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;... --&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;nav&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;aside&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="audio-einbettungen"&gt;Audio-Einbettungen&lt;/h3&gt;&#10;&lt;p&gt;Audio-Embeds sind diese kleinen Player-ähnlichen Dinger auf einer Website, die oft zu einer Streaming-Plattform oder ähnlichem weiterleiten. Ich brauche sie für einige meiner Audio-Projekte wie dieses &lt;a href="https://blog.schallbert.de/projects/mobfobamp/"&gt;mobfobamp&lt;/a&gt;: nur eine Überschrift und der Player darunter.&lt;/p&gt;&#10;&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/embed-audio.jpg" alt="Image: embed-audio" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;&lt;a href="https://stackoverflow.com/a/63807971/13757172" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Dieser Beitrag&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; hat mir sehr geholfen, so dass ich es einfach in das &lt;code&gt;_include&lt;/code&gt; einfügen konnte, es ein wenig modifizieren konnte, und es funktionierte auf Anhieb.&lt;/p&gt;&#10;&lt;h3 id="favicon"&gt;Platzierung des Favicons&lt;/h3&gt;&#10;&lt;p&gt;Ich folgte &lt;a href="https://ptc-it.de/add-favicon-to-mm-jekyll-site" 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;, um ein Favicon für die Website zu platzieren.&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/2021-06-13_favicon.jpg" alt="Image: favicon" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Der Pfad des Favicons muss unabhängig von dem Ordner, in dem sich die gerenderte Seite befindet, gültig sein. Ich musste &lt;code&gt;../&lt;/code&gt; entsprechend der Tiefe der Seiten in der Ordnerstruktur der Website hinzufügen, um sicherzustellen, dass das Favicon nicht nur auf der Startseite, sondern auch auf Unterseiten angezeigt werden kann.&lt;/p&gt;&#10;&lt;h3 id="optimierung-für-suchmaschinen"&gt;Optimierung für Suchmaschinen&lt;/h3&gt;&#10;&lt;p&gt;Da meine Website einige Wochen lang nicht von Suchmaschinen gefunden wurde, beschloss ich, eine so genannte &amp;ldquo;SEO&amp;rdquo; (Suchmaschinenoptimierung) durchzuführen. Deshalb fügte ich &lt;a href="http://jekyll.github.io/jekyll-seo-tag" target="_blank" rel="noopener noreferrer" class="external-link"&gt;jekyll-seo-tag&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; hinzu und überarbeitete z.B. alle meine Links nach &lt;a href="https://jsinibardy.com/optimize-seo-jekyll#seo-101" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Juliette Sinibardy&amp;rsquo;s SEO 101&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Danach habe ich meine Webseite dem Web-Analyse-Audit von Lighthouse unterzogen. &lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/thissite/lighthouse_audit.jpg" alt="Image: Lighthouse Audit Ergebnisse" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Für einen ersten Versuch sieht es meiner Meinung nach ziemlich gut aus. Das größte Problem auf der Leistungsseite scheint der CSS-Overhead zu sein.&lt;/p&gt;&#10;&lt;h3 id="sicherheitsrichtlinie-für-inhalte"&gt;Sicherheitsrichtlinie für Inhalte&lt;/h3&gt;&#10;&lt;p&gt;Der Lighthouse-Audit hat gezeigt, dass es eine gute Idee wäre, externe Skripte - wie sie die Basisversion meines Themes enthält - einzuschränken. Also habe ich eine Content Security Policy (CSP) wie folgt eingeführt:&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-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;webrick&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;headers&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;Content-Security-Policy&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;default-src &amp;#39;self&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:#ae81ff"&gt;script-src &amp;#39;self&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:#ae81ff"&gt;connect-src &amp;#39;self&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:#ae81ff"&gt;img-src &amp;#39;self&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:#ae81ff"&gt;style-src &amp;#39;self&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:#ae81ff"&gt;font-src &amp;#39;self;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Mit dieser strikten Auslegung werden Inhalte Dritter gar nicht mehr geladen (aber leider meine eigenen teils auch nicht).&lt;/p&gt;&#10;&lt;p&gt;Anfänglich wurde das Hintergrundbild meiner Homepage nicht angezeigt, meine eingebetteten Audiodateien konnten nicht abgespielt werden und alle Icons, die von extern geladen wurden, waren ebenfalls weg. Das Hinzufügen von &lt;code&gt;media-src: 'self';&lt;/code&gt; und etwas mehr Lektüre in &lt;a href="https://www.jakobwillforss.com/post/content-security-policy-for-font-awesome-on-netlify" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Jakob Wilforss&amp;rsquo; Blog&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und vor allem &lt;a href="https://web.dev/strict-csp/?utm_source=lighthouse&amp;amp;utm_medium=lr" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Lighthouses eigene Vorschläge&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; halfen bei der Lösung dieses Problems. Mit Hilfe der &lt;em&gt;Web developer Tools&lt;/em&gt; des Browsers habe ich die fehlenden Ressourcen ermittelt, die das Theme im Hintergrund zu laden versucht, so dass mein aktualisiertes CSP wie folgt aussieht:&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-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;webrick&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;headers&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;Content-Security-Policy&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;default-src &amp;#39;self&amp;#39; &amp;#39;unsafe-inline&amp;#39; https://cdn.jsdelivr.net/; &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;script-src &amp;#39;self&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:#ae81ff"&gt;connect-src &amp;#39;self&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:#ae81ff"&gt;img-src &amp;#39;self&amp;#39; &amp;#39;unsafe-inline&amp;#39; https://upload.wikimedia.org/wikipedia/commons/ https://raw.githubusercontent.com;&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;style-src &amp;#39;self&amp;#39; &amp;#39;unsafe-inline&amp;#39; https://cdn.jsdelivr.net/;&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;font-src &amp;#39;self&amp;#39; &amp;#39;unsafe-inline&amp;#39; https://cdn.jsdelivr.net/; &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;media-src &amp;#39;self&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:#ae81ff"&gt;object-src &amp;#39;none&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:#ae81ff"&gt;base-uri &amp;#39;none&amp;#39;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;Ende 2021 bin ich vom Nachladen von Inhalten über Drittanbieter (oben namentlich Font Awesome und Wikimedia) auf die eigene, statische Bereitstellung übergegangen. Somit gibt es keine Dritten mehr, die beim Aufruf meiner Seite etwas von Ihnen mitbekommen - und die Seite lädt etwas schneller. Meine Content Security Policy habe ich wieder auf die Obere der beiden Darstellungen zurückgedreht, &lt;code&gt;media-src 'self';&lt;/code&gt; aber beibehalten.&lt;/p&gt;&#10;&lt;h2 id="fehler"&gt;Fehler&lt;/h2&gt;&#10;&lt;p&gt;Hier sammele ich Fehler, deren Lösung trivial erscheint, die mich aber dennoch ganze Abende gekostet haben.&lt;/p&gt;&#10;&lt;h3 id="jekyll-serve-utf-8-incompatible-character-encoding"&gt;Jekyll serve: &lt;code&gt;UTF-8&lt;/code&gt; incompatible character encoding&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/thissite/bundle_error.jpg" alt="Image: bundle error when front matter is missing" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Man gibt &lt;code&gt;bundle exec jekyll serve&lt;/code&gt; ein und erhält einen seltsamen &lt;code&gt;UTF-8&lt;/code&gt; &amp;ldquo;incompatible character encoding&amp;rdquo; Fehler? Die Ursache ist einfach: Eine der zu konvertierenden Dateien hat höchstwahrscheinlich keinen Header, der in das &amp;ldquo;Front matter&amp;rdquo; der Seite übersetzt werden kann, die aus dieser Datei generiert wird. Einfach die übliche Kopfzeile zur fraglichen Datei hinzufügen und schon funktioniert es wieder:&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-yml" data-lang="yml"&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:#f92672"&gt;title&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;Feeds and Speeds database&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:#f92672"&gt;description&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;for (light) CNC milling machines&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:#f92672"&gt;header&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;image&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;blah/testimage.jpg&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:#ae81ff"&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="webseite-wird-nicht-dargestellt-404"&gt;Webseite wird nicht dargestellt: &lt;code&gt;404&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;Dieser Fehler war besonders fies versteckt, denn auf meinem Rechner als &lt;code&gt;localhost&lt;/code&gt; wird die Seite einwandfrei dargestellt - nur &amp;ldquo;in Produktion&amp;rdquo; erscheint, wenn ich die Seite aufrufe, gar nichts.&lt;/p&gt;&#10;&lt;p&gt;Ursache: neuerdings verwende ich das &lt;strong&gt;permalink:&lt;/strong&gt; Attribut in der Frontmatter von Seiten, die übersetzt werden sollen und auf einer tieferen Ebene liegen - für diese möchte ich die Links kurz halten, daher bekommen sie manuell erstellte Permalinks. Nur wenn ich ihnen ein &amp;ldquo;trailing slash&amp;rdquo; gebe, lassen sie sich auch online aufrufen. Beispiel:&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-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Falsch:&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:#f92672"&gt;title&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;This Page won&amp;#39;t be displayed&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:#f92672"&gt;permalink&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;/projects/pageerror&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;---&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&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:#75715e"&gt;# Richtig:&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:#f92672"&gt;title&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;This Page will be displayed!&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:#f92672"&gt;permalink&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;/projects/nopageerror/&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;# Wird auf dem deutschen Blog zu:&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;h2 id="website-hosting"&gt;Website-Hosting&lt;/h2&gt;&#10;&lt;p&gt;Diese Seite ist selbst ein normales Gitea-Repository. Es wird automatisch mit einer von Github Actions entlehnten Aktion gebaut, sobald Änderungen zum &lt;code&gt;Main&lt;/code&gt; des Repositorys hinzugefügt werden.&lt;/p&gt;&#10;&lt;h3 id="update-jan-2024"&gt;Update Jan-2024&lt;/h3&gt;&#10;&lt;p&gt;Inzwischen bin ich von Github-Pages nach Gitea &lt;a href="https://blog.schallbert.de/projects/move-blog-to-own-server/"&gt;umgezogen&lt;/a&gt; und hoste meine Webpräsenz nun selber. Dennoch sind alle Inhalte dieser Seite weiterhin als Repository verfügbar und können leicht von mir gewartet und auf Stand gebracht werden. Außerdem &lt;a href="https://blog.schallbert.de/server-protection/"&gt;schütze ich die Site nun vor unautorisiertem Zugriff&lt;/a&gt;&lt;/p&gt;&#10;&lt;h2 id="zusammenfassung"&gt;Zusammenfassung&lt;/h2&gt;&#10;&lt;p&gt;Sich damit vertraut zu machen wie Jekyll die Seite erstellt, wie das Theme mit meinen Befehlen interagiert und wie man das Verhalten des Themes überschreibt, hat einige Stunden gekostet. Aber ich denke, dass diese Seite eine klare Struktur und ein mehr oder weniger &amp;ldquo;professionelles&amp;rdquo; Aussehen besitzt - und ich musste nicht eine einzige Zeile JavaScript oder HTML schreiben, um das zu erreichen. Toll!&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Inhalt in Markdown &lt;code&gt;*.md&lt;/code&gt; geschrieben&lt;/li&gt;&#10;&lt;li&gt;Grundlegende Konfiguration der Seite in YAML &lt;code&gt;_config.yml&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Seitenlayouts in html und jinja &lt;code&gt;*.html&lt;/code&gt;, abgelegt in &lt;code&gt;_layouts&lt;/code&gt; und &lt;code&gt;_includes&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Seitenvariablen und Aussehen in SASS &lt;code&gt;*.scss&lt;/code&gt;, automatische Generierung von css-Dateien&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="lust-mitzumachen-oder-beiträge-beizusteuern"&gt;Lust mitzumachen oder Beiträge beizusteuern?&lt;/h2&gt;&#10;&lt;p&gt;Sie haben Ideen, Kritik oder interessante Dinge, die Sie mir mitteilen möchten? Nehmen Sie gern über meine &lt;a href="https://github.com/Schallbert/schallbert.github.io/discussions" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Diskussions-Seite&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Kontakt auf.&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>MobFobAmp</title><link>https://blog.schallbert.de/projects/mobfobamp/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/mobfobamp/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/unfolded-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: MobFobAmp Unfolded"&#10; title="MobFobAmp" /&gt;&#10;&lt;p&gt;Ein zweiter Prototyp meines mobilen Instrumentenverstärkers, dieses mal mit klappbarem Design. &lt;code&gt;28l&lt;/code&gt; Box mit &lt;code&gt;17cm&lt;/code&gt; Tiefmitteltöner. &lt;code&gt;15W&lt;/code&gt; Verstärkerleistung. &lt;code&gt;7.3kg&lt;/code&gt; inkl. Akku. Geeignet für E-Bass, Akustikgitarre, E-Piano.&lt;/p&gt;&#10;&lt;h2 id="projekt-steckbrief"&gt;Projekt-Steckbrief&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Schwierigkeitsgrad: herausfordernd 4/5&lt;/li&gt;&#10;&lt;li&gt;Kosten: ~140€ ohne Akku&lt;/li&gt;&#10;&lt;li&gt;Zeitaufwand: ~15h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="vorstellung"&gt;Vorstellung&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: amp folded, flat. Enclosure is less than 10cm strong, the speaker back protrudes by 8cm."&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/folded_lying.jpg" alt="Image: amp folded, flat. Enclosure is less than 10cm strong, the speaker back protrudes by 8cm." 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: amp folded, standing by itself."&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/folded_standing.jpg" alt="Image: amp folded, standing by itself." 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: amp unfolded, side view, the speaker chassis protrudes from the speaker face."&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/unfolded_side.jpg" alt="Image: amp unfolded, side view, the speaker chassis protrudes from the speaker face." 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: amp unfolded, top view. An 18mm thick beech panel keeps the enclosure shut and is sturdy enough to be sat upon."&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/unfolded_top.jpg" alt="Image: amp unfolded, top view. An 18mm thick beech panel keeps the enclosure shut and is sturdy enough to be sat upon." class="hugo-gallery__thumb-img" loading="lazy" /&gt;&#10; &lt;/a&gt;&lt;a class="hugo-gallery__thumb" href="#gallery-full-4" aria-label="Image: amp unfolded, front view. Battery and amplifier board are visible at the combo&amp;#39;s front."&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/unfolded.jpg" alt="Image: amp unfolded, front view. Battery and amplifier board are visible at the combo&amp;#39;s front." class="hugo-gallery__thumb-img" loading="lazy" /&gt;&#10; &lt;/a&gt;&lt;a class="hugo-gallery__thumb" href="#gallery-full-5" aria-label="Image: view inside the amp: Nothing but air and a strengthening panel."&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/inside_spacer.jpg" alt="Image: view inside the amp: Nothing but air and a strengthening panel." class="hugo-gallery__thumb-img" loading="lazy" /&gt;&#10; &lt;/a&gt;&lt;/div&gt;&lt;div class="hugo-gallery__caption"&gt;Ein paar Bilder von meinem mobilen, klappbaren Bassverstärker 'MobFobAmp'.&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/mobfobamp/folded_lying.jpg" alt="Image: amp folded, flat. Enclosure is less than 10cm strong, the speaker back protrudes by 8cm." class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__figcaption"&gt;Benötigt zusammengeklappt extrem wenig Platz&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/mobfobamp/folded_standing.jpg" alt="Image: amp folded, standing by itself." class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__figcaption"&gt;Flaches Design, leicht zu tragen&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/mobfobamp/unfolded_side.jpg" alt="Image: amp unfolded, side view, the speaker chassis protrudes from the speaker face." class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__figcaption"&gt;28l Gehäusevolumen wenn ausgeklappt, geschlossenes Design&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/mobfobamp/unfolded_top.jpg" alt="Image: amp unfolded, top view. An 18mm thick beech panel keeps the enclosure shut and is sturdy enough to be sat upon." class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__figcaption"&gt;Durch eine Höhe von 53cm auch als Hocker verwendbar&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure id="gallery-full-4" class="hugo-gallery__figure"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/unfolded.jpg" alt="Image: amp unfolded, front view. Battery and amplifier board are visible at the combo&amp;#39;s front." class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__figcaption"&gt;17cm Bass-Mittenlautsprecher, 4Ah-Akkupack für lange Laufzeit&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure id="gallery-full-5" class="hugo-gallery__figure"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/inside_spacer.jpg" alt="Image: view inside the amp: Nothing but air and a strengthening panel." class="hugo-gallery__full-img" /&gt;&lt;figcaption class="hugo-gallery__figcaption"&gt;Ein Blick nach Innen&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&#10;&lt;/section&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;audio controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/audio/MobFobAmp_sound_sample.mp3" type="audio/mp3"&gt;&#10; Your browser does not support the audio element.&#10; &lt;/audio&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;MobFobAmp Soundbeispiel&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;audio controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/audio/MobFobAmp_sound_ABA_closedOpen.mp3" type="audio/mp3"&gt;&#10; Your browser does not support the audio element.&#10; &lt;/audio&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;A/B/A Vergleich geschlossenes vs. offenes Gehäuse&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;audio controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/audio/MobFobAmp_piano_balade.mp3" type="audio/mp3"&gt;&#10; Your browser does not support the audio element.&#10; &lt;/audio&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;MobFobAmp und Piano, externer Vorverstärker, maximale Lautstärke&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Alle Aufnahmen habe ich mit meinem Smartphone gemacht. Dennoch bringen sie den Klang des Verstärkers realistisch rüber.&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;&lt;/p&gt;&#10;&lt;h2 id="mobfobamp-motivation"&gt;&amp;ldquo;MobFoBamp&amp;rdquo;: Motivation&lt;/h2&gt;&#10;&lt;p&gt;Um die Nachteile meines &lt;a href="https://blog.schallbert.de/projects/carsubamp/"&gt;vorherigen Prototypen&lt;/a&gt; zu beheben, wollte ich es mal mit einem größeren Lautsprecher und mehr Volumen probieren. Leider macht einem so eine große Box ja das Tragen schwer, also dachte ich mir, wenn man quasi die Luft raus ließe für den Transport, wäre das nicht eine Lösung&amp;hellip;?&lt;/p&gt;&#10;&lt;h2 id="die-idee-klappbox"&gt;Die Idee: Klappbox&lt;/h2&gt;&#10;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://thumb.wikimedia.org/wikipedia/commons/thumb/0/0c/Faltbehaelter01.jpg/330px-Faltbehaelter01.jpg" alt="Image: folding box" /&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Eine Klappbox&lt;/span&gt;&lt;a href="https://commons.wikimedia.org/wiki/File:Faltbehaelter01.jpg" class="attr-link" aria-label="Attribution 1"&gt;&#10; &lt;sup class="attr-id"&gt;[1]&lt;/sup&gt;&#10; &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;In Anlehnung an das Konzept einer Faltbox, wie es hier gezeigt wird, beschloss ich, dass mein Combo zum einfachen Transport zusammenklappbar sein sollte. Ich entschied mich zunächst für eine geschlossene Konstruktion, damit ich mich nicht um die Berechnung von Lüftungsöffnungen, Resonanzfrequenzen und Gruppenlaufzeiten kümmern musste.&lt;/p&gt;&#10;&lt;p&gt;Ich musste mir Gedanken darüber machen, wie ich Lautsprecher und Elektronik integrieren konnte, ohne die Möglichkeit zu verlieren, das Gehäuse vollständig zusammenzulegen.&lt;/p&gt;&#10;&lt;h3 id="abmessungen"&gt;Abmessungen&lt;/h3&gt;&#10;&lt;p&gt;Ich habe die Breite des Gehäuses so gewählt, dass es bei Bedarf bequem in meine Bass-Gigbag passt. Alle anderen Abmessungen ergaben sich von selbst - die Höhe musste mindestens doppelt so groß sein wie die Breite, damit der obere und untere Deckel im zusammengeklappten Zustand untergebracht werden konnte (aber nicht zu hoch, um die Steifigkeit nicht zu reduzieren). Die &amp;ldquo;Tiefe&amp;rdquo; musste kleiner sein als &amp;ldquo;0,5*Breite&amp;rdquo; - (Materialstärke + Scharnierdicke), sonst würde das Falten nicht funktionieren, weil die Seiten im Weg wären.&lt;/p&gt;&#10;&lt;h3 id="sound-hardware"&gt;Sound-Hardware&lt;/h3&gt;&#10;&lt;p&gt;Für den Leistungsverstärker habe ich mich für das Modul entschieden, das ich schon früher gekauft habe.&#10;Ich wählte einen &lt;a href="https://www.visaton.de/en/products/drivers/woofers/w-170-s-4-ohm" target="_blank" rel="noopener noreferrer" class="external-link"&gt;6,5-Zoll-Hi-Fi-Tieftöner (50W, 4Ohm)&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; mit niedriger Resonanzfrequenz, passend zu meinem versiegelten Gehäuse.&lt;/p&gt;&#10;&lt;h3 id="simulation"&gt;Simulation&lt;/h3&gt;&#10;&lt;p&gt;Dann führte ich einige &lt;a href="https://lautsprechershop.de/tools/t_box_closed.htm" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Simulationen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; durch, um herauszufinden, ob Gehäuse und Lautsprecher gut zusammenpassen würden.&lt;/p&gt;&#10;&lt;p&gt;Der geschätzte Qtc-Wert läge bei 0,63 anstelle des empfohlenen Wertes von 0,71 (optimale Sprungantwort), so dass das Gehäuse etwas überdämpft wäre und &amp;ldquo;straffer&amp;rdquo; klingen würde, aber im Vergleich zu einer optimalen Einstellung an Volumen am unteren Ende verlieren würde. Der Bau einer Box ist leider immer ein Kompromiss zwischen Klang, Leistung, Größe, Gewicht, Frequenzgang und Lautstärke, wobei ich Prioritäten setzen und bestimmte Attribute etwas vernachlässigen musste.&lt;/p&gt;&#10;&lt;h2 id="ausprobieren"&gt;Ausprobieren&lt;/h2&gt;&#10;&lt;p&gt;Als ich nach etwa 15 Stunden mit dem Bau des Gehäuses fertig war, war ich sehr erfreut zu sehen, dass es luftdicht schließt und solide und stabil steht. Für das Ausklappen brauchte ich weniger als 60 Sekunden, für das Zurückklappen in den ursprünglichen Zustand sogar weniger als 30 Sekunden. Da der Maximalpegel immer noch etwas niedrig war, fügte ich dem Setup ein Kompressorpedal hinzu, das einen Gain-Regler hatte, den ich als Vorverstärker verwenden konnte.&lt;/p&gt;&#10;&lt;p&gt;Der Klang dieses Geräts ist erstaunlich. Straffe Bässe, warme Mitten und subtile, aber brillante Höhen passen sehr gut zum charakteristischen Klang der Bassgitarre. Die maximale Lautstärke ist begrenzt, aber sie reicht immer aus, um mit einer Akustikgitarre mithalten zu können. Ich benutze ihn ständig als Übungsverstärker.&lt;/p&gt;&#10;&lt;h2 id="klangqualität"&gt;Klangqualität&lt;/h2&gt;&#10;&lt;p&gt;Der wichtigste Aspekt eines Combos ist sicherlich, wie er klingt. Durch das faltbare Design kann er in zwei verschiedenen Setups betrieben werden.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Geschlossen: Straffer und ausgewogener Klang, guter Tiefbass-Punch. Ohne Vorverstärker ist der maximale Pegel OK, um mit einer Akustikgitarre zu spielen&lt;/li&gt;&#10;&lt;li&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/mobfobamp/bottom_vented.jpg" alt="Bild: Belüftetes Setup, Deckel geöffnet" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;Belüftet: Unterer Deckel um &lt;code&gt;3cm&lt;/code&gt; geöffnet, wobei der Riegel das Gehäuse gegen die Bodenplatte drückt. Viel lauterer Klang mit ausgeprägten tiefen Mitten, weniger ausgewogen und etwas dröhnend, Maximalpegel auf dem Niveau von Piano gespielt &lt;em&gt;f&lt;/em&gt;.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Ich bin sehr zufrieden mit der Klangqualität, die MobFobAmp bietet. Die Option, &amp;ldquo;offen&amp;rdquo; zu spielen, erhöht die Flexibilität und lässt den Sound wirklich hervorstechen (mit den oben erwähnten Nachteilen).&lt;/p&gt;&#10;&lt;p&gt;Ich habe auch einige Messungen durchgeführt, um zu sehen, wie nahe das Design an einen idealen Frequenzgang herankommt, indem ich etwas &lt;a href="https://en.wikipedia.org/wiki/White_noise" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Weißes Rauschen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; an MobFobAmp schickte und dann den Tonausgang mit einem Mikrofon aufnahm. Zum Vergleich habe ich eine Spektrumanalyse im Frequenzbereich angezeigt. Eine vollkommen neutrale Tonausgabe würde die orangefarbene Linie bei 0 dB für das gesamte Spektrum treffen. &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/frequency_response.jpg" alt="Frequenzgang des weißen Rauschens"&gt;{: .align-center} Man kann sehen, dass die &lt;code&gt;-3dB&lt;/code&gt;-Grenze oder der nutzbare Frequenzbereich von &lt;code&gt;30Hz&lt;/code&gt; bis &lt;code&gt;~1.5kHz&lt;/code&gt; reicht.&lt;/p&gt;&#10;&lt;h2 id="add-ons"&gt;Add-Ons&lt;/h2&gt;&#10;&lt;h3 id="9v-spannungsversorgung-für-pedalboard"&gt;9V Spannungsversorgung für Pedalboard&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/mobfobamp/7809.jpg" alt="Bild: 9V Linearregler TO-220" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Ich benutze einige Effektpedale beim Üben, und ich wollte sie auch benutzen können, wenn ich MobFobAmp mitnehme. Deshalb habe ich beschlossen, eine &amp;ldquo;+9V&amp;rdquo;-Versorgungsschiene mit Hohlstecker (Außen = +) an Bord zu haben.&#10;Also kaufte ich einen seit Jahrzehnten auf dem Markt befindlichen linearen Spannungsregler aus der 78xx-Serie namens 7809 in einem TO-220-Design, der bis zu 1,5A Strom bei 9V liefern kann und einen Eingangsspannungsbereich von 11-35V akzeptiert. Das Bauteil kann mit wenigen zusätzlichen Teilen verwendet werden, wie in der Abbildung rechts gezeigt. Auf diese Weise kann ich jetzt ein Pedalboard mit der Batterie des MobFobAmp betreiben!&lt;/p&gt;&#10;&lt;h3 id="piezo-hochtöner"&gt;Piezo-Hochtöner&lt;/h3&gt;&#10;&lt;p&gt;Als ich zum ersten Mal einen Drumcomputer mit mobfobamp benutzt habe, hatte ich das Gefühl, dass die Höhen einfach nicht ausreichen. Während sie für einen E-Bass perfekt waren, klangen die Hi-Hat, die Becken und sogar die Snare des Drumcomputers dumpf und muffig. Also bestellte ich einen einfachen Piezo-Treiber, den ich parallel zum Hauptlautsprecher anschließen konnte - das funktioniert perfekt. Ich fügte einen &amp;ldquo;2W/320Ohm&amp;rdquo;-Widerstand in Reihe mit dem Treiber hinzu, um die Pegel für meine Ohren genau richtig einzustellen. Das Ergebnis ist großartig.&lt;/p&gt;&#10;&lt;h2 id="mobfobamp-vorteile"&gt;MobFobAmp Vorteile:&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Zusammenklappbar auf sehr kompakte Maße&lt;/li&gt;&#10;&lt;li&gt;Geringes Gewicht: &lt;code&gt;7.3kg&lt;/code&gt; inkl. Batterie, &lt;code&gt;6.9kg&lt;/code&gt; ohne&lt;/li&gt;&#10;&lt;li&gt;Läuft &lt;code&gt;&amp;gt;20h&lt;/code&gt; mit einer Akkuladung als Übungsverstärker und moderater Lautstärke&lt;/li&gt;&#10;&lt;li&gt;Läuft &lt;code&gt;&amp;gt;3h&lt;/code&gt; mit einer Batterieladung bei maximaler Lautstärke&lt;/li&gt;&#10;&lt;li&gt;Klingt sehr gut für Bass, E-Piano oder Akustikgitarre&lt;/li&gt;&#10;&lt;li&gt;Tiefe Töne bis hinunter zu &amp;ldquo;B0&amp;rdquo; funktionieren gut und klingen druckvoll&lt;/li&gt;&#10;&lt;li&gt;9V DC Ausgang für Vorverstärker oder Effektgeräte&lt;/li&gt;&#10;&lt;li&gt;Kann im &amp;ldquo;Bass Reflex Mode&amp;rdquo; für mehr Tiefmitten betrieben werden&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="zu-verbessern"&gt;Zu verbessern:&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Ohne Piezo-Hochtöner wirken die Höhen matt und gedämpft&lt;/li&gt;&#10;&lt;li&gt;Geringer Headroom der Endstufe: Wenn der externe Vorverstärker zu stark aufgedreht wird, verzerrt die Endstufe vor allem tiefe Töne, erst mit Flattern, dann mit dem typischen Klang der Transistorsättigung&lt;/li&gt;&#10;&lt;li&gt;Die chaotische Verkabelung des Prototyps ist immer noch anfällig für mechanische Beschädigungen, daher wäre ein zusätzliches Gehäuse für die Elektronik schön&lt;/li&gt;&#10;&lt;li&gt;Natürlich reicht die Lautstärke und Leistung nicht aus, um ein Schlagzeug oder eine Band zu beschallen.&lt;/li&gt;&#10;&lt;li&gt;Ein Schutzgitter für den Lautsprecher wäre schön&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="mobfobamp---diy-replica"&gt;MobFobAmp - DIY Replica&lt;/h2&gt;&#10;&lt;p&gt;Natürlich ist der Nachbau erwünscht! Unten ein Link zur Anleitung.&#10;&lt;a href="https://blog.schallbert.de/mobfobamp-diy/"&gt;Mobfobamp DIY-Anleitung&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;Piano gespielt von &lt;a href="https://www.facebook.com/feliks.weber" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Feliks Weber&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&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;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>PC-Upgrade</title><link>https://blog.schallbert.de/projects/casemodding-htpc/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/casemodding-htpc/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/htpc/htpc-upgrade-thumb.avif"&#10; class="post-cover"&#10; alt="Image: Components for the PC upgrade"&#10; title="PC-Upgrade" /&gt;&#10;&lt;h2 id="projekt-steckbrief"&gt;Projekt-Steckbrief&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Schwierigkeit: Amateur 3/5&lt;/li&gt;&#10;&lt;li&gt;Kosten: ~500€&lt;/li&gt;&#10;&lt;li&gt;Zeitaufwand: ~20h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="ziel-und-motivation"&gt;Ziel und Motivation&lt;/h2&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/htpc/htpc-before-upgrade.avif" alt="Image: My HTPC before the upgrade" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Im Jahr 2006 hatte ich mir einen Spiele-PC gegönnt. Als Gehäuse kaufte ich ein HTPC-Gehäuse. Es ist relativ flach und passt in das Rack einer Stereoanlage. 2015 tauschte ich ein erstes mal das Innenleben, als mir die Hardware zu langsam wurde. Ende 2025 war es wieder so weit: Das Netzteil zeigte sich mit der Leistungsaufnahme der neuen Grafikkarte überfordert, die Festplatten liefen voll und Spiele nicht mehr stabil.&lt;/p&gt;&#10;&lt;h2 id="umbauten"&gt;Umbauten&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/htpc/htpc-after-upgrade.avif" alt="Image: Insides of the refreshed PC" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Kurz: Es muss neue Hardware her. Doch diese erzeugt viel mehr Abwärme als die Vorherige. Zur Optimierung des Luftstroms sind daher einige Umbauten vorzunehmen. Klingt doch nach einem tollen Projekt mit vielen Facetten, die dieses Blog ausmachen: Mechanik, Elektronik, Software, CNC-fräsen.&lt;/p&gt;&#10;&lt;h3 id="neue-komponenten"&gt;Neue Komponenten&lt;/h3&gt;&#10;&lt;p&gt;Einzubauen sind&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;650W Enermax Netzteil ersetzt 400W&lt;/li&gt;&#10;&lt;li&gt;Neues Motherboard MSI B550&lt;/li&gt;&#10;&lt;li&gt;AMD Ryzen5600x ersetzt Intel i5-6500&lt;/li&gt;&#10;&lt;li&gt;32GB DDR4 RAM statt 16GB&lt;/li&gt;&#10;&lt;li&gt;1TB PCIE SSD statt SATA SSDs&lt;/li&gt;&#10;&lt;li&gt;140mm Lüfter in 5.25&amp;quot; Schacht&lt;/li&gt;&#10;&lt;li&gt;2x60mm Lüfter in hintere Auslässe&lt;/li&gt;&#10;&lt;li&gt;96mm Lüfter für CPU, neuer Heatpipe-Kühler&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Die meisten Komponenten besorge ich mir gebraucht. Nur Netzteil, Platte und Lüfter sind Neuware. Doch schnell zeigt sich, dass ich das Gehäuse modifizieren muss: Die Grafikkarte kollidiert mit dem 3.5&amp;quot;-Schacht, wo bis dato Festplatten und Kartenlesegerät untergebracht waren. Der große 140mm-Lüfter passt nur an die Stelle, wo sich der 5.25&amp;quot;-Schacht befindet.&lt;/p&gt;&#10;&lt;p&gt;Also müssen beide Schächte entfernt oder mit dem Trennschleifer bearbeitet werden, um Platz zu schaffen. Außerdem benötige ich eine Montagevorrichtung für den Lüfter vor der 5.25&amp;quot;-Öffnung. Hingucker für das Gehäuse wäre es jetzt noch, die Laufwerkklappe automatisch steuern zu können. Dann würde sie nur bei laufendem Lüfter unter hoher Rechenlast geöffnet und im Normalbetrieb geschlossen bleiben.&lt;/p&gt;&#10;&lt;h3 id="anpassung-des-gehäusedeckels"&gt;Anpassung des Gehäusedeckels&lt;/h3&gt;&#10;&lt;p&gt;In meinem Artikel zum Thema &lt;a href="https://blog.schallbert.de/milling-secc/"&gt;Fräsen von SECC-Stahlblech&lt;/a&gt; zeige ich, wie man den Gehäusedeckel zwecks besserer Belüftung der Grafikkarte mit zusätzlichen Schlitzen ausstattet.&lt;/p&gt;&#10;&lt;h3 id="35-schacht"&gt;3.5&amp;quot;-Schacht&lt;/h3&gt;&#10;&lt;p&gt;{&amp;lt; image src=&amp;quot;/assets/images/htpc/floppy-bay-modified.avif&amp;quot; alt=&amp;ldquo;Image: Cutout in 3.5&amp;rdquo; bay&amp;quot; position=&amp;ldquo;right&amp;rdquo; &amp;gt;}&#10;Die Grafikkarte ist so lang, dass sie &lt;code&gt;1cm&lt;/code&gt; Überlappung mit dem 3.5&amp;quot;-Schacht besitzt. Zum Glück lässt sich der Schacht im Handumdrehen ausbauen. Also nehme ich den Trennschleifer zur Hand und bringe einen Ausschnitt in die Halterung ein. Anschließend bohre ich ein paar zusätzliche Löcher in die Seiten des Schachtes, sodass meine 2.5&amp;quot;-SSDs auf der von der Grafikkarte abgewandten Seite des Schachtes befestigt werden können. Auch der Schachtdeckel erhält ein weiteres Loch zur Befestigung am Gehäuse, da zwei Rastnasen durch den Ausschnitt für die Grafikkarte entfallen.&lt;/p&gt;&#10;&lt;p&gt;Der Kartenleser nutzt die volle Tiefe des Schachtes aus. Ich fürchte also schon, ihn gar nicht wieder montiert zu bekommen. Probehalber öffne ich sein Gehäuse und siehe da: Nur das vorderste Drittel wird tatsächlich von Leiterplatten belegt. Der hintere Teil ist anscheinend nur dafür da, Befestigungsmöglichkeiten zu bieten. Also trenne ich den hinteren Teil des Kartenlesergehäuses kurzerhand ab und baue alles wieder zusammen, sodass er sich nun passend im Schacht anbringen lässt.&lt;/p&gt;&#10;&lt;h3 id="525-schacht"&gt;5.25&amp;quot;-Schacht&lt;/h3&gt;&#10;&lt;p&gt;Der Schacht ist hoch genug, um zwei Laufwerke aufzunehmen. Ich benutze aber kaum noch CDs bzw. DVDs - und wenn, dann tut es auch ein externes Laufwerk. Den leeren Laufwerkkäfig kann ich also ausbauen und stattdessen an seiner Stelle einen Lüfter montieren. Dafür entwerfe ich im CAD eine Adapterplatte, die Befestigungsmöglichkeiten und Ausschnitte für den Lüfter enthält.&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/htpc/drive-bay-mod.avif" alt="Image: mounting adapter to attach a case fan to the drive bay" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h3 id="lüfterplatzierung"&gt;Lüfterplatzierung&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/htpc/aux-fan.avif" alt="Image: External 140mm fan" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Ich setze Gehäuselüfter gezielt ein, um zwei voneinander getrennte Luftströmungen im PC-Gehäuse zu erzeugen: Die CPU-Netzeil-Strömung verläuft an der Gehäusefront über einen 140mm-Lüfter an den CPU-Kühler, von wo aus die Warme Luft durch Netzteil und zwei weitere 60mm-Gehäuselüfter hinaus geleitet werden.&lt;/p&gt;&#10;&lt;p&gt;Der Luftstrom zur Grafikkarte wird über einen außerhalb des Gehäuses verbauten 140mm-Lüfter gestützt. Die Grafikkarte selbst fördert die erwärmte Luft direkt über den Gehäusedeckel nach draußen.&#10;Um den externen Lüfter anzuschließen, schneide ich ein Loch in eine Slotblende für PCI-Einbauschächte. Dort hinein passt genau die Buchse eines Lüftersteckers. Mittels Heißkleber sichere ich die Buchse in der Blende und schließe sie sonst den Lüfter auch an einen AUX-FAN Anschluss an.&lt;/p&gt;&#10;&lt;h2 id="klappensteuerung"&gt;Klappensteuerung&lt;/h2&gt;&#10;&lt;p&gt;Nun soll die Frontklappe noch automatisch öffnen, sobald der Lüfter dreht. Ist zwar komplizierter als wenn ich das von Hand mache, aber dieses Gimmick macht das Gehäuse doch erst richtig interessant. Ich entferne also das Klappenschloss und füge der Adapterplatte eine Halterung für ein Modellbauservo hinzu, welches die Klappe öffenn und schließen können soll.&lt;/p&gt;&#10;&lt;h3 id="elektronik"&gt;Elektronik&lt;/h3&gt;&#10;&lt;p&gt;Das Servo benötigt eine Ansteuerung. Außerdem möchte ich die Klappenzustand wie den Lüfter auch temperaturabhängig regeln, sodass ich die an dem Lüfter angelegte Spannung einlesen muss. Hierfür setze ich den Microcontroller &lt;a href="https://www.microchip.com/en-us/product/ATTINY44" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ATTINY44&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ein. Die Schaltung habe ich nur mit den allernötigsten Komponenten bestückt:&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/htpc/servo-control-pcb.avif" alt="Iamge: servo control circuit board" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Der Microcontroller gibt ein pulsweitengesteuertes Signal an den Servo aus. Dessen Spannungsversorgung wird über einen Elko gepuffert. Die Energieversorgung für den Gehäuselüfter leite ich über zwei Steckerleisten weiter. Dort greife ich das Signal mittels &lt;a href="https://de.wikipedia.org/wiki/Spannungsteiler" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Spannungsteiler&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ab und führe es einem Analogeingang zu. Ansonsten ist nur ein weiterer Widerstand verbaut, der den RESET-Eingang des Microcontrollers auf &lt;code&gt;VCC&lt;/code&gt; zieht und ihn so in Betrieb hält.&lt;/p&gt;&#10;&lt;h3 id="software-anforderungen"&gt;Software-Anforderungen&lt;/h3&gt;&#10;&lt;p&gt;Die Anforderungen an die Software sind einfach und klar:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Wenn der Lüfter steht, soll die Klappe schließen&lt;/li&gt;&#10;&lt;li&gt;Ist der Lüfter in Betrieb, soll die Klappe öffnen&lt;/li&gt;&#10;&lt;li&gt;Damit die Klappe nicht zu oft hin- und her fährt, soll der Klappenzustand mit etwas Verzögerung geändert werden und auch nur dann, wenn der Lüfter in der Zwischenzeit seinen Zustand beibehalten hat.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Die daraus entstandene Software ist auf &lt;a href="https://github.com/Schallbert/ServoKlappe/tree/main" target="_blank" rel="noopener noreferrer" class="external-link"&gt;meinem Github-Account&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; frei verfügbar.&lt;/p&gt;&#10;&lt;h3 id="software-hauptschleife"&gt;Software: Hauptschleife&lt;/h3&gt;&#10;&lt;p&gt;Bei der Ansteuerung des Modellbauservos mache ich es mir einfach: Ich verwende die &lt;a href="https://docs.arduino.cc/libraries/servo/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Servo-Bibliothek&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; von Arduino.&lt;/p&gt;&#10;&lt;p&gt;Das Programm macht nicht viel. Ich initialisiere das Servo &lt;code&gt;servoHatch&lt;/code&gt;, welches später im Programm angesteuert wird, definiere ein paar wenige Variablen und lege fest, auf welchem Pin der Spannungsteiler vom Lüfter liegt. Die Hauptschleife begnügt sich damit, die Spannung am Lüfter einzulesen und basierend darauf zu entscheiden, ob sich die Klappe bewegen sollte.&lt;/p&gt;&#10;&lt;p&gt;Die Auswertung übergebe ich dann der Klappensteuerung. Anschließend wartet das Programm &lt;code&gt;15ms&lt;/code&gt; und läuft dann erneut los.&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;Servo servoHatch; &lt;span style="color:#75715e"&gt;// create Servo object to control a servo&#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;uint16_t&lt;/span&gt; fanVoltage{&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;}; &lt;span style="color:#75715e"&gt;// variable to read the value from the analog pin&#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;bool&lt;/span&gt; open{false};&#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; &lt;span style="color:#a6e22e"&gt;setup&lt;/span&gt;() {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; pinMode(pinFanVoltage, 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;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; &lt;span style="color:#a6e22e"&gt;loop&lt;/span&gt;() {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; fanVoltage &lt;span style="color:#f92672"&gt;=&lt;/span&gt; analogRead(pinFanVoltage); &lt;span style="color:#75715e"&gt;// reads the value of the fan voltage (value between 0 and 1023)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; open &lt;span style="color:#f92672"&gt;=&lt;/span&gt; mapHatchState(fanVoltage);&#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; setHatch(open);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; delay(&lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;); &lt;span style="color:#75715e"&gt;// waits for the servo to get there&#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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="software-hysterese"&gt;Software: Hysterese&lt;/h3&gt;&#10;&lt;p&gt;Mit der Funktion &lt;code&gt;mapHatchState&lt;/code&gt; lege ich den Sollzustand der Klappe in Abhängigkeit der Spannung am Lüfter fest.&lt;/p&gt;&#10;&lt;p&gt;Liegt die Spannung lange genug unter etwa &lt;code&gt;4.8V&lt;/code&gt;, so schließt die Klappe oder behält ihren geschlossenen Zustand bei. Liegt sie darüber, soll die Klappe geöffnet werden. Der Dauer des Zustandswechsels hängt von der Spannung selbst ab; bei Werten weit ab vom Schwellwert läuft der Aggregator schneller voll und der Klappenzustand wird ebenfalls zügiger verändert.&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:#66d9ef"&gt;bool&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;mapHatchState&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;uint16_t&lt;/span&gt; voltage){&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;#define maxint 32767 - 1024&#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;#define minint -32768 + 1024&#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;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int16_t&lt;/span&gt; vAggregate{&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;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; hatchOpen{false};&#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;// Divider VCC 33k T 22k GND, 12V fan will yield 4.8V at divider --&amp;gt; 983 digit&#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;// So a value of 410 is roughly 5V which allows Fan start + hatch open&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; vAggregate &lt;span style="color:#f92672"&gt;=&lt;/span&gt; vAggregate &lt;span style="color:#f92672"&gt;+&lt;/span&gt; voltage &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;410&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; (vAggregate &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; maxint) {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; vAggregate &lt;span style="color:#f92672"&gt;=&lt;/span&gt; maxint;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hatchOpen &lt;span style="color:#f92672"&gt;=&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; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (vAggregate &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; minint) {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; vAggregate &lt;span style="color:#f92672"&gt;=&lt;/span&gt; minint;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hatchOpen &lt;span style="color:#f92672"&gt;=&lt;/span&gt; false;&#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;return&lt;/span&gt; hatchOpen;&#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;h3 id="software--klappe-bewegen"&gt;Software : Klappe bewegen&lt;/h3&gt;&#10;&lt;p&gt;Die Funktion &lt;code&gt;setHatch()&lt;/code&gt; prüft den gewünschten Zustand der Klappe und bewegt sie, falls sie vom aktuellen Zustand abweicht, dort hin.&#10;Dabei wird dem Servo mittels &lt;code&gt;write()&lt;/code&gt; ein Zielwinkel vorgegeben, der eingestellt und gehalten werden soll. Die Bewegung geschieht graduell und über die Iteration von &lt;code&gt;hatchState&lt;/code&gt;, da die Klappe sich mir sonst viel zu ruckartig und schnell bewegt.&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/htpc/htpcfancomp.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;Test der Klappensteuerung durch Verstellen der Lüfterspannung&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&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:#66d9ef"&gt;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;setHatch&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; open){&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; hatchState{&lt;span style="color:#ae81ff"&gt;180&lt;/span&gt;}; &lt;span style="color:#75715e"&gt;// variable to capture current position, default closed&#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;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; wasMoving{false};&#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; isMoving{false};&#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; (open &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; (hatchState &lt;span style="color:#f92672"&gt;&amp;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; hatchState&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; isMoving &lt;span style="color:#f92672"&gt;=&lt;/span&gt; true;&#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; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#f92672"&gt;!&lt;/span&gt;open &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; (hatchState &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;180&lt;/span&gt;)) {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hatchState&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; isMoving &lt;span style="color:#f92672"&gt;=&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;&#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; (&lt;span style="color:#f92672"&gt;!&lt;/span&gt;wasMoving &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; isMoving) {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; servoHatch.&lt;span style="color:#a6e22e"&gt;attach&lt;/span&gt;(portServo); &lt;span style="color:#75715e"&gt;// connect servo on start&#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;else&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (wasMoving &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span style="color:#f92672"&gt;!&lt;/span&gt;isMoving) {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; servoHatch.&lt;span style="color:#a6e22e"&gt;detach&lt;/span&gt;(); &lt;span style="color:#75715e"&gt;// disconnect servo on finish&#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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; servoHatch.&lt;span style="color:#a6e22e"&gt;write&lt;/span&gt;(hatchState);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; wasMoving &lt;span style="color:#f92672"&gt;=&lt;/span&gt; isMoving;&#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;Die Funktionen &lt;code&gt;attach()&lt;/code&gt; und &lt;code&gt;detach()&lt;/code&gt; definieren den Signalausgang zum Servo, ordnen also den Pin des Microcontrollers intern dem Servo so zu, wie er tatsächlich verdrahtet ist. Bei meinen Experimenten fand ich heraus, dass das Servo im Stillstand bei &lt;code&gt;detached&lt;/code&gt; nur wenig Energie benötigt - viel weniger, als wenn ich es &lt;code&gt;attached&lt;/code&gt; lasse und nur kein weiteres Bewegungssignal sende. Daher melde ich es für jede Bewegung an und danach wieder ab.&lt;/p&gt;&#10;&lt;h2 id="ergebnis"&gt;Ergebnis&lt;/h2&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/htpc/htpc-upgrade-complete.avif" alt="Image: HTPC insides with all upgrades applied" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Ich erhalte einen schön aufgeräumten, modernen PC. Mittels diverser Bloatware-Lösungen (die ich nach getaner Arbeit direkt wieder deinstalliere) schaffe ich es nach einigem Fluchen, alle LED-Farben aufeinander abzustimmen. Besonderes Highlight ist die automatische Klappe, welche bei höherer CPU-Last samt Lüfter aktiviert wird.&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/htpc/htpc-case-complete.avif" alt="Image: HTPC case with all upgrades applied" /&gt;&lt;/figure&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>Seifenbutler</title><link>https://blog.schallbert.de/projects/seifenbutler/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/seifenbutler/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/soapbutler/soapbutler-thumb.jpg"&#10; class="post-cover"&#10; alt="SoapButler: a soap holder for the shower rail"&#10; title="Seifenbutler" /&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: ~250€&lt;/li&gt;&#10;&lt;li&gt;Zeitaufwand: mehr als ein Jahr&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="die-geschichte-vom-seifenbutler"&gt;Die Geschichte vom Seifenbutler&lt;/h2&gt;&#10;&lt;p&gt;Begleiten Sie mich hier über eine Zeitraum von mehr als einem Jahr, in welchem ich beständig an ein- und demselben Projekt arbeite. Dabei geht es von einer Idee mit wenigen Anforderungen hin zur Vorserienreife. Ich erläutere meine Prototypenarbeit, gehe ein wenig auf die vielen Rückschläge ein und nähere mich am Ende einem - wie ich meine - guten Produkt an.&lt;/p&gt;&#10;&lt;h2 id="idee"&gt;Idee&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;Mai 2022&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;Vor Kurzem bin ich beim Duschen von flüssigem Shampoo auf ein Shampoo bzw. Seife in fester Form umgestiegen. Da ich keine Ablage für die Seife hatte, wurde sie gleich mitgeduscht. So verbrauchte sie sich schneller, wurde tagelang nicht trocken und klebte auch noch am Rand der Dusche fest.&lt;/p&gt;&#10;&lt;p&gt;Ich wollte keinen normalen Seifenhalter kaufen, für den man Löcher in die gefliesten Wände bohren müsste. Also beschloss ich, einen Seifenhalter zum Anklemmen für die Duschstange zu entwerfen. Ich würde das Teil über dem Duschkopf anbringen, damit es nicht mehr Wasser als nötig ausgesetzt ist.&lt;/p&gt;&#10;&lt;p&gt;Außerdem soll der Seifenhalter im Handumdrehen und ohne Werkzeug zu montieren sein.&lt;/p&gt;&#10;&lt;h2 id="v1-v3-erste-entwürfe-in-hpl"&gt;V1-V3: Erste Entwürfe in HPL&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;Juli 2022&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;Also entwarf ich meinen eigenen Halter. Ich wollte ein einfaches, rundes Design mit einem Klemmloch zur Befestigung an der Duschstange und einem abgesenkten Bereich, in dem die Seife bzw. das feste Shampoo untergebracht werden sollte. Durch einige Schlitze im Boden sollte das Wasser abtropfen und die Seife an der Luft trocknen können, wenn sie nicht benutzt wird. Ich wollte ein Befestigungselement im Inneren des Teils und eine Mutter auf der Innenseite anbringen, damit der Durchmesser des Lochs für die Duschstange verringert und der Seifenbutler - so der Name für das gute Stück - auf diese Weise festgeklemmt werden 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/soapbutler/clamping_explained.jpg" alt="Image: soapbutler V1&amp;#39;s clamping mechanism explained " /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Um dies zu ermöglichen war es sinnvoll, zwei Bauteile zu fertigen - ein oberes, das die Seife an Ort und Stelle hält und Platz für die Aufnahme der Befestigungsmechanik bietet und ein unteres, das Ablaufschlitze für überschüssiges Wasser und die Aufnahme der anderen Hälfte der Befestigungsmechanik aufweist.&lt;/p&gt;&#10;&lt;h3 id="freecad-skizzen"&gt;FreeCAD: Skizzen&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/soapbutler/freecad_partbodydwg.jpg" alt="Image: FreeCAD Teil, Körper, Skizze" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;FreeCAD-Artefakte werden auf eine bestimmte Art organisiert, um den Umgang mit verschiedenen Ansichten zu vereinfachen oder die spätere zweiseitige Bearbeitung mit Hilfe der Funktion Klonen vorzubereiten. Daher erstelle ich zunächst eine Reihe von Teilen im Modus &lt;code&gt;Parts Design&lt;/code&gt;. Skizzen können in Körperelementen gruppiert werden, die wiederum in Teilelementen angeordnet sind.&lt;/p&gt;&#10;&lt;p&gt;Ich erstelle also ein Teil (gelbes Element), das &amp;ldquo;Skizzen&amp;rdquo; enthält - alle Originalzeichnungen (rote Elemente) aller Körper (blaue Elemente) des/der zu produzierenden Teils/Teile. Diese Zeichnungen sollten vollständig bestimmt &amp;ldquo;fully constrained&amp;rdquo; sein. Sie können keine Informationen zur Positionierung relativ zu anderen Komponenten enthalten. Das bedeutet, dass sich die Skizzen alle am gleichen Ort befinden. Somit sollten alle inaktiven Skizzen ausgeblendet werden um Verwechslungen zu vermeiden.&lt;/p&gt;&#10;&lt;p&gt;(Einen tieferen Einblick in meinen FreeCAD-Workflow bietet &lt;a href="https://blog.schallbert.de/freecad-get-started/"&gt;dieser Blog-Artikel&lt;/a&gt;.)&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/soapbutler/freecad_scetches.jpg" alt="Image: FreeCAD Scetch" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h3 id="freecad-teile"&gt;FreeCAD: Teile&lt;/h3&gt;&#10;&lt;p&gt;Dann erstelle ich Teile, die später die zu schneidenden Komponenten enthalten, z.B. &amp;ldquo;clampingHoles&amp;rdquo;, &amp;ldquo;soapButler&amp;rdquo;, &amp;ldquo;wallHolder&amp;rdquo;. Sie enthalten jeweils eine Untermenge der Skizzen, aber keine Informationen zur endgültigen Positionierung. Zuletzt erstelle ich ein Teil für jeden Auftrag, den meine Produktion auszuführen hat, z. B. &lt;code&gt;job00_clamping&lt;/code&gt;, &lt;code&gt;job01_upperMill&lt;/code&gt;, &lt;code&gt;job02_3dCut&lt;/code&gt;, &lt;code&gt;job03_lowerFinish&lt;/code&gt;. Diese klonen Körperkomponenten, fügen aber Platzierungsinformationen hinzu, enthalten gespiegelte Teile für das 2-seitige Fräsen oder bieten einen Umriss, um die XY-0 richtig einzustellen.&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/soapbutler/freecad_parts.jpg" alt="Image: FreeCAD Teil" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Der Vorteil einer solchen Projektkonfiguration besteht darin, dass, wenn eine Original-Skizze aus irgendeinem Grund geändert werden muss, alle geklonten Teile automatisch ebenfalls geändert werden. Zudem kann man bei geschickter Organisation erreichen, dass Maße der Teile einfach in der Tabelle geändert werden können und sich dann die gesamte Teilegeometrie darauf anpasst, ohne dass manuell nachgearbeitet werden muss.&lt;/p&gt;&#10;&lt;h3 id="bearbeitung"&gt;Bearbeitung&lt;/h3&gt;&#10;&lt;p&gt;Für meinen ersten Prototyp habe ich Hochdrucklaminat als Material gewählt, da es sehr widerstandsfähig ist, ab und zu nass werden kann und ziemlich gut aussieht. Für die Bearbeitung habe ich Fräser mit niedriger Spiralsteigung verwendet, um die Vibrationen beim Schneiden des zähen HPL-Materials unter Kontrolle zu halten. Ich habe mich für beschichtete Einschneider-Hartmetallfräser entschieden, die ich vor einiger Zeit gekauft habe (für das Schneiden von Aluminium). Ich benutzte Exzenterspanner, um das Teil an seinem Platz zu halten, und nahm nur zwei zusätzliche Niederhalter zur Hand, damit das Teil auf dem Maschinenbett blieb.&lt;/p&gt;&#10;&lt;p&gt;Ich hatte einen Fehler im CAM-Programm, für dessen Behebung ich ein paar Minuten brauchte: Ich hatte die Werkzeugwege für die Spirale nach innen und nicht nach außen eingestellt. Das Problem dabei ist, dass die Spirale mit Fortschreiten des Schnittes immer schwammiger und instabiler wird. Ich konnte die Vibrationen nur dadurch in Schach halten, dass ich den Fräsvorgang häufig unterbrach und Klebeband an den Stellen anbrachte, an denen der Schnitt bereits eingebracht war.&lt;/p&gt;&#10;&lt;h3 id="bewertung"&gt;Bewertung&lt;/h3&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/soapbutler/machining_v2.jpg" alt="Bild: Bearbeitung V2 von soapbutler" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Die Herstellung des ersten Prototyps lief ganz OK. Die Maschine brummte zuversichtlich, und abgesehen von einigen kleinen Fehlern (Befestigungslöcher im unteren Teil zu eng, Länge der Klammerbefestigung falsch angegeben) war ich mit dem Ergebnis einigermaßen zufrieden.&lt;/p&gt;&#10;&lt;p&gt;Ich brauchte etwa 20 Minuten, um das Teil auszulösen und die Anbindungen zu entfernen. Dies erschien mir als deutlich verbesserungswürdig.&lt;/p&gt;&#10;&lt;h2 id="v4-fehlschlag-mit-billigem-hpl"&gt;V4: Fehlschlag mit billigem HPL&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;Oktober 2022&lt;/strong&gt;&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/soapbutler/cam_chamfer_issue.jpg" alt="Bild: Fasenproblem bei schmalen Schlitzen" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Ich habe die Fasenfunktion in meiner CAM noch nie verwendet und dachte, dies wäre eine gute Gelegenheit für einen Test, da die Teileecken bei meinem ersten Prototyp ziemlich scharf waren. Da ich nur einen 4mm Schaftfräser verwende, erschien mir der 90° 10mm Fasenfräser zu groß, zumal ich nach außen versetzen musste, um die Spitze des Fasenfräsers nicht zu verschleißen. Deshalb wählte ich einen 30°-Winkel, was sich schnell als Fehler herausstellte, da die Ecken damit kaum weicher wurden.&lt;/p&gt;&#10;&lt;p&gt;Ich vergaß auch, dass nur die Außenseite der Teile von einer abgeschrägten Kante profitieren würde. Ich hingegen wendete die Fase auf der Innenseite an, was der Verarbeitungsqualität abträglich war. Außerdem stellte ich fest, dass die neue HPL-Platte, aus der ich die Prototypen herstellte, nicht ganz plan auflag, so dass einige Teile des Umrisses nicht vollständig durchgefräst waren. Dies führte wiederum zu einer geringeren Qualität und zu erheblichen manuellen Nacharbeiten.&lt;/p&gt;&#10;&lt;h2 id="v5-korrekturen-im-cam-und-bessere-verarbeitungsqualität"&gt;V5: Korrekturen im CAM und bessere Verarbeitungsqualität&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;November 2022&lt;/strong&gt;&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/soapbutler/clamping2_explained.jpg" alt="Image: soapbutler V2&amp;#39;s clamping mechanism explained" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Ich finde die Freiarmklammer mit offenem Befestigungselement nicht so schön anzuschauen, weil sie Asymmetrisch ist. Außerdem würde sie wegen des engen Spalts schwer zu reinigen sein. Nach einigem Nachdenken kam ich auf die Lösung einer &amp;ldquo;Mittelklemme&amp;rdquo;: Das Befestigungselement würde gegen ein Stück Gummi drücken, das direkt an der Duschstange anliegt, um die Seifenablage in Position zu halten.&lt;/p&gt;&#10;&lt;h3 id="fasen"&gt;Fasen&lt;/h3&gt;&#10;&lt;p&gt;Nachdem ich die Zeichnungen entsprechend aktualisiert hatte, erstellte ich ein weiteres, gespiegeltes Teil in FreeCAD im Modus &amp;ldquo;Draft&amp;rdquo;. Dieses Teil klont das Originalteil am Umriss und ist für das zweiseitige Fräsen bestimmt, wobei das Teil um seine linke obere Kante (XY-Null) gedreht wird. Auf diese Weise ist die Fase dann richtig umzusetzen, wenn ich sicherstellen kann, dass sich der XY-Nullpunkt nicht bewegt.&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/soapbutler/freecad_clones.jpg" alt="Image: Clone function in FreeCAD to mirror parts" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Das hat in der Praxis sehr gut funktioniert, und ich konnte auch das Anbringen von Senkbohrungen für die unteren Befestigungselemente automatisieren, was die Bauqualität erhöht. Das Ergebnis ist unten zu sehen.&lt;/p&gt;&#10;&lt;h3 id="bewertung-1"&gt;Bewertung&lt;/h3&gt;&#10;&lt;p&gt;Nachdem dieses fünfte Teil bearbeitet war musste ich leider feststellen, dass die Melaminbeschichtung an der Schnittkante leichte Ausrisse aufwies. Außerdem bemerkte ich während der Bearbeitung den Geruch nach verbranntem Material. Ein kurzer Blick auf den Fräser offenbarte die Ursache: Er war nach nur einer Stunde Betriebszeit trotz Beschichtung stumpf geworden. Das macht die Arbeit mit HPL ziemlich teuer und frustrierend. Außerdem war die Zykluszeit für diese Version recht lang: 15 Minuten für ein Teil. Das würde nie wirtschaftlich werden&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;.&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/soapbutler/soapbutler_v5.jpg" alt="Bild: Soapbutler_v5 front view with fastener" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Deshalb habe ich versucht, alternative Materialien für den Seifenbutler zu finden.&#10;Inzwischen sollten die Prototypen ein paar Monate lang in der Dusche ausprobiert werden, um Langzeiterfahrungen zu sammeln. Ist HPL geeignet für den Einsatz im Feuchtraum? Kriecht das Wasser zwischen den beiden Schichten hindurch? Ist es leicht zu reinigen?&lt;/p&gt;&#10;&lt;h2 id="v6-fehlschlag-mit-birke-multiplex"&gt;V6: Fehlschlag mit Birke-Multiplex&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;Mai 2023&lt;/strong&gt;&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/soapbutler/cad_v3.jpg" alt="Image: CAD updated for 1-piece approach made wich stronger sheets" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Auf der Suche nach neuen Materialien besorgte ich wasserfeste Holzplatten, verleimt im Multiplexverfahren. Da sie mit 15mm deutlich stärker war als das vorher verwendete Material, vereinfachte ich das Design in eine Ausführung, die aus nur einem Teil besteht und hinten zentral über eine Madenschraube verfügt, die die Klemmung an der Duschstange übernehmen sollte.&lt;/p&gt;&#10;&lt;p&gt;Das Loch für die Madenschraube würde ich nach der Fertigung auf der CNC von Hand bohren.&lt;/p&gt;&#10;&lt;p&gt;Leider jedoch stellte sich das Material auf der CNC als ungeeignet für das Fräsen der Spirale heraus. Ich bekam zahlreiche Ausrisse und hatte Schwierigkeiten, das Werkstück niederzuhalten. Die Konturen meines Seifenbutlers waren für das kreuzverleimte Material schlicht zu fein bzw. die Faserlänge des Holzes zu groß.&lt;/p&gt;&#10;&lt;p&gt;Die fünf gefertigten Teile wanderten direkt in den Restmüll.&lt;/p&gt;&#10;&lt;h2 id="v7-fehlschlag-mit-aluminium-verbundmaterial"&gt;V7: Fehlschlag mit Aluminium-Verbundmaterial&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;Juli 2023&lt;/strong&gt;&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/soapbutler/seifenbutler_v7.jpg" alt="Image: Seifenbutler V7 in CAD, showing two mirrored parts that open on opposite sides to have the shower rail added in between" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Ich kehrte nun zum Design als Zweiteiler zurück und überlegte, ob Aluminium-Verbundmateial durch seine Korrosionsbeständigkeit nicht ebenfalls für meine Seifenablage geeignet wäre.&lt;/p&gt;&#10;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/soapbutler/seifenbutler_v7_fabricated.jpg" alt="Image: Seifenbutler V7, manufactured in Aluminium composite material" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Kurzerhand fertigte ich einen neuen Entwurf an&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;, der die Eigenschaften des neuen Materials versucht, so gut wie möglich zu berücksichtigen. Nebenbei überlegte ich mir, dass durch das leichtere Material jetzt endlich eine Halterung über Magnete möglich sein müsste und sah entsprechende Senkungen im Rahmen dafür vor.&lt;/p&gt;&#10;&lt;p&gt;Ich hatte die Idee, dass ein Teil von je einer Seite der Duschstange aus angelegt und dann über die Magnete zusammengehalten würden - damit bin ich endlich die lästige Klemmschraube los und habe ein sehr einfaches Design.&lt;/p&gt;&#10;&lt;p&gt;Leider jedoch erwies sich das Konzept als völlig ungeeignet. Die Haltekraft der Magnete ist nicht hoch genug und die Klemmung funktioniert nur über das Abkippen des Seifenhalters nach vorne, welches einen seltsam schiefen Eindruck an der Duschstange ergibt.&lt;/p&gt;&#10;&lt;p&gt;Also: Weitertüfteln!&lt;/p&gt;&#10;&lt;h2 id="v8-umstieg-auf-acrylglas-spannfutter"&gt;V8: Umstieg auf Acrylglas, Spannfutter&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;Juli 2023&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;Nach vielen Stunden am Rechner für den Entwurf kam ich auf ein neues Klemmprinzip, abgeschaut vom Bohrfutter eines Akkuschraubers: Drei im 120°-Winkel angebrachte, bewegliche Klemmvorrichtungen würden - mittels eines Kabelbinders zusammengeführt - Duschstangen beliebigen Durchmessers festhalten.&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/soapbutler/seifenbutler_v8.jpg" alt="Image: Seifenbutler V8, made of acrylic glass with chuck" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h3 id="fertigung"&gt;Fertigung&lt;/h3&gt;&#10;&lt;p&gt;Ich stieß auf vielfältige Schwierigkeiten in der Fertigung, verursacht hauptsächlich durch die teils sehr kleinen Komponenten, welche zudem auch noch Kerben für die Führung der Kabelbinder aufweisen mussten. Hinzu kam bei mir die Einsicht, dass Entwurf und Anfertigung leichtgängiger Passungen ohne merkliches Spiel für mich eine große Herausforderung sind und eine Menge erfolgloser Versuche benötigt, bis ich am Ziel bin.&lt;/p&gt;&#10;&lt;p&gt;Durch die Verwendung verschieden großer (2mm, 4mm) Fräser und Optimierung der Fräsreihenfolge konnte ich die Teile jedoch schließlich erfolgreich und in guter Qualität herstellen. &lt;a href="https://blog.schallbert.de/milling-small-parts/"&gt;Siehe dazu mein Artikel zum Thema Kleinteile Fräsen.&lt;/a&gt; Einzig die Spirale bereitet mir noch Kopfzerbrechen, denn auch in Acrylglas möchte sie dem Fräser am liebsten ausweichen. Dadurch sind ihre Kanten recht scharf.&lt;/p&gt;&#10;&lt;p&gt;Der Zusammenbau gestaltete sich zum Glück einfach - man benötigt nur eine Schraube und etwas Plastikklebstoff. Nur das Einfädeln des Kabelbinders will geübt sein.&lt;/p&gt;&#10;&lt;h3 id="praxistest"&gt;Praxistest&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/soapbutler/seifenbutler_v8_fabricated.jpg" alt="Image: Seifenbutler V8 after some months on duty" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Ich hatte diese Variante testweise für ein paar Monate in meiner Dusche. Da die Schraube nur verzinkt war, begann sie leider recht schnell, zu korrodieren. Merke: Edelstahl verwenden.&lt;/p&gt;&#10;&lt;p&gt;Die Stabilität des Seifenbutlers ist weiter verbesserungswürdig. Der Kabelbinder lässt etwas Spiel, sodass mit wenig Kraftaufwand Neigungen von rund 10° gegenüber der Senkrechten der Duschstange möglich sind. Dabei knarzt es ganz schön zwischen Ober- und Unterteil. Da muss noch nachgebessert werden.&lt;/p&gt;&#10;&lt;p&gt;Außerdem würde ich den Klemmmechanismus gern wieder etwas vereinfachen.&lt;/p&gt;&#10;&lt;h2 id="v9-gegossenes-acrylglas-seifenbutler-abnehmbar"&gt;V9: Gegossenes Acrylglas, Seifenbutler abnehmbar&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;Juli 2023&lt;/strong&gt;&#10;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/soapbutler/seifenbutler_v9.jpg" alt="Image: Seifenbutler V9 with two separate parts for clamping" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&lt;/p&gt;&#10;&lt;p&gt;Diese Weiterentwicklung hält Ober- und Unterschale vorn mit Magneten zusammen, welche sich unsichtbar im Material versenken lassen. Im Klemmbereich hinten befinden sich zwei Einsätze, die wie bei einer Schublade in eine passende Aussparung des Seifenbutler passen und ihn so zusammenhalten.&lt;/p&gt;&#10;&lt;p&gt;Ich habe mehrere Prototypen benötigt, bis ich die Binderführung soweit hatte, dass der Kabelbinder einfach hinten in den Seifenbutler gesteckt wird, er sich von selbst um die Duschstange legt und schließlich wieder zum Vorschein kommt, wo er dann zum Festzurren bereit liegt.&lt;/p&gt;&#10;&lt;p&gt;Dies ist das erste Design, was meine Anforderungen hinsichtlich Haltekraft und Einfachheit der Montage erfüllt. Leider ist die Fertigung komplex und die Toleranzen müssen eng gehalten werden, da der Seifenhalter entweder zu leicht aus der Klemmung herausrutscht oder aber durch zu enge Passungen kaum angebracht werden kann.&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/soapbutler/soapbutler_v9.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;Seifenbutler zip tie demonstration&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h2 id="v10-verbessertes-fertigungsverfahren"&gt;V10: Verbessertes Fertigungsverfahren&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;August 2023&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;Mit ein paar Optimierungen entspricht diese Version der V9. Ich erreichte eine bessere Wiederholbarkeit in der Fertigung und kann Toleranzen für Passungen etwas großzügiger auslegen.&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/soapbutler/seifenbutler_v10.jpg" alt="Image: Seifenbutler V10, multiple parts on a sheet for manufacturing" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Mit diesem Design hole ich mir erste Rückmeldungen aus dem Bekanntenkreis. Einhellige Meinung: Kabelbinder wirken wie ein Provisorium und können den Kunden nicht zugemutet werden.&lt;/p&gt;&#10;&lt;p&gt;Also zurück ans Reißbrett.&lt;/p&gt;&#10;&lt;h2 id="v11-stecksystem-l-nase-in-hpl"&gt;V11: Stecksystem &amp;lsquo;L-Nase&amp;rsquo; in HPL&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;August 2023&lt;/strong&gt;&#10;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/soapbutler/seifenbutler_v11.jpg" alt="Image: Seifenbutler V11 with L-type puzzle clamping" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&lt;/p&gt;&#10;&lt;p&gt;Ein weiterer Versuch in HPL, dieses Mal mit Vielzahnfräsern. Sie raspeln das Material eher als dass sie es schneiden und sind für die Bearbeitung abrasiver, faserreicher Materialien wie CFK/GFK ausgelegt. Die hohe Zähigkeit von HPL gepaart mit der schlechten Wärmeableitung jedoch lassen die Fräser nach nur zwei gefertigten Prototypen komplett stumpf werden. Also sind sie auch keine Lösung für effiziente Arbeit in HPL für mich.&lt;/p&gt;&#10;&lt;p&gt;Das Design ist wieder mehrteilig und erreicht die Klemmung im letzten Update über eine hinten montierte Schraube, die nach Zusammensetzen von Vorder- und Rückteil angezogen wird. Dadurch wird der Seifenhalter insgesamt länger, was ich vom Design her nicht optimal finde.&lt;/p&gt;&#10;&lt;p&gt;Die Fertigungsqualität hingegen habe ich inzwischen gut im Griff, finde ich. Die Schnitte sind sauber und auch die Qualität der Kanten ist sehr ansprechend.&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/soapbutler/seifenbutler_v11_fabricated.jpg" alt="Image: Seifenbutler V11 made of HPL, disassembled" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Durch die schnelle Abnutzung der Fräser ist es für mich sehr schwer, in eine leichte Übermaßpassung für die Steckverbindung herzustellen. Allzu leicht geschieht es bei meinen Versuchen, dass der Fräser zu schnell an Durchmesser verliert und die Teile gar nicht oder nur mit roher Gewalt ineinander gesteckt werden können.&lt;/p&gt;&#10;&lt;p&gt;Vielleicht kann ich dieses Problem durch eine andere Passungsgeometrie lösen?&lt;/p&gt;&#10;&lt;h2 id="v12-stecksystem-brio"&gt;V12: Stecksystem &amp;lsquo;Brio&amp;rsquo;&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;September 2023&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;Hier verwende ich wieder Acrylglas, diesmal die extrudierte Variante. So sind die Dicketoleranzen gegenüber der gegossenen Variante sehr gering und meine Z-Nullpunkte sind stets passend. Dies ist prima für anzubringende Fasen, die endlich bei jedem Teil gleich breit ausfallen und dem Material eine hohe Wertigkeit verleihen.&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/soapbutler/seifenbutler_v12.jpg" alt="Image: CAD drawing for the plug-in system of Seifenbutler V12" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Nun habe ich auch eine Lösung für die Spirale: Doppelseitige Bearbeitung. Dadurch steigt zwar der Aufwand für mich, allerdings verbringe ich lieber mehr Zeit an der Maschine, als mich später über mangelnde Qualität zu ärgern.&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/soapbutler/seifenbutler_v12_fabricated.jpg" alt="Image: Prototype of Seifenbutler V12" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Mein Vorgehen: Ich fräse die Spirale fast ganz durch und lasse nur zarte &lt;code&gt;0.3mm&lt;/code&gt; stehen. Auf der Rückseite dann verwende ich den Fasenfräser und fahre die Spirale von innen nach außen ab. Zusätzlich sichere ich die Unterseite mit Klebeband, sodass mein Vakuumtisch die Spirale sicher halten kann und Schwingungen vermieden werden.&lt;/p&gt;&#10;&lt;p&gt;Das Design sieht ein Rändelrad vor, sodass die Klemmung ohne Werkzeug erfolgen kann. Da die Klemmung starke Zugkräfte auf die Verbinder ausüben und sie daher aus Stabilitätsgründen genau in der Achse der Duschstange positioniert sein müssen, habe ich auch hier ein leichtes Abkipp-Problem. Dieses verstärkt sich, je kleiner der Durchmesser der Duschstange ist.&lt;/p&gt;&#10;&lt;p&gt;Mit einer weiteren Variante konnte ich das Abkippen vermeiden, allerdings sind die Zapfen dann nicht mehr verdeckt. Dies wiederum hat zur Folge, dass dort jetzt Duschwasser eindringt und Rückstände hinterlassen kann.&lt;/p&gt;&#10;&lt;p&gt;Ich bin fast da. Trotzdem kann ich vielleicht noch etwas verbessern.&lt;/p&gt;&#10;&lt;h2 id="v13-klemmautomatik-und-nullserienfertigung"&gt;V13: Klemmautomatik und Nullserienfertigung&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;Oktober 2023&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;In Version 13 versuche ich, die Vorteile der vorgehenden Varianten V9-V12 zu bündeln und dabei die Nachteile (Abkippen, Kabelbinder, Montage mit Werkzeug, instabil) so gut wie möglich zu umgehen.&lt;/p&gt;&#10;&lt;h3 id="neues-klemmkonzept"&gt;Neues Klemmkonzept&lt;/h3&gt;&#10;&lt;p&gt;Ich verfolge daher noch ein neues Klemmkonzept. Ich möchte alle handelsüblichen Duschstangen bei der Montage berücksichtigen und muss daher zwischen &lt;code&gt;18mm&lt;/code&gt; und &lt;code&gt;25mm&lt;/code&gt; sicher klemmen. Aus den vorherigen Versionen ist mir ebenfalls klar geworden, dass zusätzlicher Druck von zentral hinter der Duschstange ausgeübt werden muss, um ein Abkippen des Seifenbutlers nach vorn zu verhindern.&lt;/p&gt;&#10;&lt;p&gt;Zusätzlich versuche ich, die Verriegelung um die Duschstange über eine Drehbewegung zu realisieren. Der Drehpunkt ist dabei die Mitte der Spirale, was drei weitere Schraubverbindungen am Umfang des Seifenbutlers erforderlich macht. Mit &lt;code&gt;V13.9&lt;/code&gt; schaffe ich endlich den Durchbruch.&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/soapbutler/seifenbutler_v13.jpg" alt="Image: CAD drawing for V13.9 with multiple stages for closing." /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Eine Klemmautomatik wird durch das Zusammenschieben von Ober- und Unterschale betätigt, sodass der Seifenbutler sich dem Durchmesser der Duschstange stufenlos anpasst. Die Arretierung findet über ein Rändelrad an der Unterseite statt, wo eine schiefe Ebene am Fanghaken unbeabsichtigtes Öffnen verhindert.&lt;/p&gt;&#10;&lt;h3 id="fertigung-1"&gt;Fertigung&lt;/h3&gt;&#10;&lt;p&gt;Dieses Design ist unglücklicherweise sehr komplex. Es besteht aus fünf Komponenten aus Acrylglas, fünf Schrauben und einer Rändelschraube mit Federring, deren Zusammenbau etwas Zeit in Anspruch nimmt.&lt;/p&gt;&#10;&lt;p&gt;Die Arbeit an der CNC beträgt über 10 Minuten pro Teil, was doch recht üppig ausfällt. Zudem habe ich einiges an Verbrauchsmaterial durch die erforderliche Werkstückniederhaltung.&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/soapbutler/seifenbutler_v13_fabricated.jpg" alt="Image: Seifenbutler V13.9" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Dennoch: Die Fertigungsqualität ist so hoch wie nie zuvor und der Seifenbutler ist jetzt kinderleicht und ohne Werkzeug binnen kurzer Zeit anzubringen.&lt;/p&gt;&#10;&lt;h2 id="fazit"&gt;Fazit&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;Januar 2024&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;Wenn ich meine CAD-Dateien so durchsehe, dann habe ich in der Summe bis jetzt &lt;code&gt;42&lt;/code&gt; Versionen meines Seifenhalters geplant, aufgeteilt in &lt;code&gt;13&lt;/code&gt; teils sehr unterschiedliche Designs. &lt;code&gt;21&lt;/code&gt; dieser Versionen haben es am Ende auch wirklich auf die CNC geschafft und &lt;code&gt;5&lt;/code&gt; von ihnen sind wiederum in einer größeren Stückzahl als &lt;code&gt;3&lt;/code&gt; hergestellt worden.&lt;/p&gt;&#10;&lt;p&gt;Ich habe vier Seifenbutler zwecks Dauertest in meiner Dusche und inzwischen werden sie von der ganzen Familie benutzt.&lt;/p&gt;&#10;&lt;p&gt;Die Anzahl Stunden, die ich mit dem Thema bis jetzt verbracht habe, kann ich gar nicht seriös zählen - es waren aber echt viele. Ich habe eine Menge über Materialien und ihre Eignung für verschiedene Teilegeometrien, Aufnahme von Kräften, ihre Bearbeitbarkeit und dafür notwendige Schnittwerte an meiner Maschine gelernt. Und auch, dass es sich lohnt, Dinge mit Biss auch über lange Zeit weiterzuverfolgen. Vielleicht nicht monetär, doch auf jeden Fall für mich als Mensch.&lt;/p&gt;&#10;&lt;p&gt;Ich möchte auch gar nicht ausschließen, weitere Optimierungen am Seifenbutler vorzunehmen - erst einmal will ich in naher Zukunft aber schauen, &lt;a href="https://blog.schallbert.de/seifenbutler-pilot-run/"&gt;ob er als Nischenprodukt markttauglich ist&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;Einen tieferen Einstieg zu den Herausforderungen bei Bearbeitung von HPL gebe ich &lt;a href="https://blog.schallbert.de/cnc-router-overload/"&gt;in diesem Blogbeitrag&lt;/a&gt;.&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;Wie man an den CAD-Bildern ab hier erkennen kann, bin ich mit meiner Zeichensoftware umgestiegen (von FreeCAD nach CADasCAM). Für 2D-Zeichnungen ist es trotz einiger Nachteile leichter zu bedienen und ich komme schneller zu Ergebnissen. Ein großer Wermutstropfen: CADasCAM ist nicht quelloffen 😥.&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>Spindel-Upgrade</title><link>https://blog.schallbert.de/projects/spindle-upgrade/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/spindle-upgrade/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/spindleupgrade/mafellvsspinogy-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: The new spindle mounted on Zerspanobert"&#10; title="Spindel-Upgrade" /&gt;&#10;&lt;h2 id="projekt-steckbrief"&gt;Projekt-Steckbrief&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Schwierigkeit: Experte 5/5&lt;/li&gt;&#10;&lt;li&gt;Kosten: ~2700€&lt;/li&gt;&#10;&lt;li&gt;Zeitaufwand: ~20h&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/bN8MKYr4dmZZd2Tmoga9Ny" frameborder="0" allowfullscreen&gt;&lt;/iframe&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;CNC Spindel-Upgrade&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Entgegen meiner Aussage &lt;a href="https://blog.schallbert.de/portal-milling/#spindle-power"&gt;hier&lt;/a&gt; habe ich mich entschieden, eine Hochfrequenz-Spindel zu kaufen. Ich musste eine Menge lesen, planen, bauen und &lt;a href="https://blog.schallbert.de/testing-hardware/"&gt;testen&lt;/a&gt; bevor ich mit der neuen Spindel endlich &amp;ldquo;Späne machen&amp;rdquo; konnte.&lt;/p&gt;&#10;&lt;h2 id="gründe-für-die-investition"&gt;Gründe für die Investition&lt;/h2&gt;&#10;&lt;p&gt;Es gibt starke und schwache Argumente für solch ein Upgrade. Kurz gesagt waren meine Argumente eher schwach, also hätte ich bei meinem bewährten Oberfräsmotor (Universalmaschine) bleiben können, deren Möglichkeiten und Grenzen nach einem Jahr im Einsatz klar auf der Hand lagen. Letztendlich war es mehr eine emotionale als eine faktenbasierte Entscheidung.&lt;/p&gt;&#10;&lt;h3 id="starke-argumente-"&gt;Starke Argumente 🥰&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Mehr Leistung: &lt;a href="https://blog.schallbert.de/cnc-router-overload/"&gt;Dieses Problem&lt;/a&gt; sollte nun nicht erneut auftreten, denn die Spindel kann jetzt deutlich höhere Zustellungen und Geschwindigkeiten fahren. Daher reduziert sich die Herstellungszeit, vor allem bei Aufträgen, bei denen viel Material abgetragen werden muss.&lt;/li&gt;&#10;&lt;li&gt;Mehr Drehmoment bei niedrigen Drehzahlen: Mein Fräsmotor konnte bei Einsatz eines Planfräsers die Drehzahl kaum halten, zudem überhitzte er sehr schnell weil sein Lüfterrad nicht genügend Luftdurchfluss ermöglichte. Durch die Fremdkühlung ist das Problem jetzt behoben.&lt;/li&gt;&#10;&lt;li&gt;Vibrationen: Ich hoffe, dass mein &lt;a href="https://blog.schallbert.de/cnc-vibrates/"&gt;Vibrationsproblem&lt;/a&gt; durch die gesteigerte Steifigkeit der Spindelmontage nun gelöst ist; das wird sich aber erst noch zeigen müssen.&lt;/li&gt;&#10;&lt;li&gt;Höhere Präzision durch bessere Maschinenlager und höhere Stabilität der Halterung&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="schwache-argumente-"&gt;Schwache Argumente 🤔&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Die Spindel ist deutlich leiser als mein vorheriger Fräsmotor.&lt;/li&gt;&#10;&lt;li&gt;Ich finde Drehstrom-Asynchronmotoren und ihr Betrieb über Frequenzumrichter (FU) super spannend.&lt;/li&gt;&#10;&lt;li&gt;Ich mag das schicke Aussehen der Spindel.&lt;/li&gt;&#10;&lt;li&gt;Ich habe mich über ein halbes Jahr lang mit den Leuten bei Spinogy unterhalten und verschiedene Lösungen diskutiert. Ich wollte sie am Ende nicht ohne Kauf zurücklassen.&lt;/li&gt;&#10;&lt;li&gt;Der Frequenzumrichter hat zusätzliche interne Sicherheitsmechanismen, die bei Auftreten eines Fehlers Beschädigung von Maschine bzw. Werkstück verhindern können&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="gegenargumente-"&gt;Gegenargumente 🤨&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Schlechte Kosten-Nutzen-Bilanz: Die Maschine ist etwas vielseitiger und schneller geworden, die Kosten aber sind deutlich gestiegen&lt;/li&gt;&#10;&lt;li&gt;Aufwärmzeit der Spindel sind nun &lt;code&gt;30 Minuten&lt;/code&gt;, eine echt lange Zeit wenn man die Maschine nicht im täglichen Einsatz hat&lt;/li&gt;&#10;&lt;li&gt;Langwierige Einarbeitung in das Fachgebiet wird benötigt. Über &lt;a href="https://assets.omron.eu/downloads/latest/manual/en/i570_mx2_users_manual_en.pdf?v=8" target="_blank" rel="noopener noreferrer" class="external-link"&gt;400 Seiten Frequenzumrichter-&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; &lt;a href="https://shop.spinogy.de/wp-content/uploads/2022/02/Operating-Manual-X22.pdf" target="_blank" rel="noopener noreferrer" class="external-link"&gt;und Spindel-Handbücher&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; waren zu lesen, Schaltpläne zu zeichnen und ein Schaltschrank aufzubauen.&lt;/li&gt;&#10;&lt;li&gt;Die Portalkonstruktion meiner CNC ist für leichte bis mittelschwere Zerspanungstätigkeiten gedacht. Wenn ich die Leistung der Spindel vollständig ausnutzen möchte, würde es die Maschine überlasten, insbesondere ihren Schwachpunkt, die Z-Achse.&lt;/li&gt;&#10;&lt;li&gt;Die Spindel wiegt das Dreifache verglichen mit dem Fräsmotor, den ich vorher im Einsatz hatte.&lt;/li&gt;&#10;&lt;li&gt;Die komplexere Ansteuerung per Frequenzumrichter fügt eine Menge potentieller Fehlerquellen hinzu.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Zusammenfassend kann ich sagen, dass die Spindel durch ihre hohe Leistungsfähigkeit nicht so richtig zum Rest des Systems passt und ihre Vorteile dadurch nicht zur Gänze ausspielen kann. Ich hoffe, dass ich die Kaufentscheidung nicht bald bereuen werde.&lt;/p&gt;&#10;&lt;h2 id="automatischer-werkzeugwechsel"&gt;Automatischer Werkzeugwechsel?&lt;/h2&gt;&#10;&lt;p&gt;Hätte ich einen gut laufenden Fachbetrieb, z.B. zur Herstellung von Produkten aus Holz, dann wäre ein Werkzeugwechsler sicher eine gute Investition.&lt;/p&gt;&#10;&lt;p&gt;Für die Prototypenherstellung hingegen trifft &lt;a href="https://blog.schallbert.de/portal-milling/#tool-changer"&gt;meine Hypothese hier&lt;/a&gt; noch immer zu. Ich benötige eine Menge verschiedener Fräser, da ich mit verschiedenen Materialien, Durchmessern und Typen experimentiere. Allein die Kosten für genügend Steilkegel-Werkzeughalter allein würden die der Spindel bereits übersteigen.&lt;/p&gt;&#10;&lt;p&gt;Stattdessen habe ich mir eine weitere Spindelmutter besorgt. Somit kann ich die Spannzangen meist in den Muttern belassen, was ein paar weitere Sekunden pro Wechselvorgang einspart.&lt;/p&gt;&#10;&lt;h2 id="kosten-"&gt;Kosten 💶&lt;/h2&gt;&#10;&lt;p&gt;Ich habe noch einige andere Entscheidungen getroffen um meinen nur gelegentlichen Betrieb der Maschine als Nebenerwerb zu berücksichtigen sowie die Kosten gering zu halten. Bei meiner Spindelkonfiguration habe ich die Option mit reduzierter Leistung gewählt und die Spezifikationen der Lager nicht erhöht (obwohl ich für kleine Fräser schon die &lt;code&gt;50.000U/min&lt;/code&gt; gebrauchen könnte), so dass die Maschine auf &lt;code&gt;30kU/min&lt;/code&gt; begrenzt ist. Ich habe Luftkühlung der wassergekühlten Variante vorgezogen und nicht in ein System zur kontinuierlichen Temperaturüberwachung investiert.&lt;/p&gt;&#10;&lt;p&gt;Dennoch betragen die Gesamtkosten des Systems mehr als das Doppelte der Spindelkosten. Obwohl der FU nicht allzu teuer ist, summieren sich abgeschirmte Motorkabel, Steckverbinder, Drehmomentschlüssel, Netzfilter, Spannzangen/Muttern, der Schaltkasten und viele zusätzliche Kleinteile ganz schön auf.&lt;/p&gt;&#10;&lt;h2 id="informationsphase"&gt;Informationsphase&lt;/h2&gt;&#10;&lt;p&gt;Um die neue Spindel zum Laufen zu bringen, musste ich ein komplettes Subsystem planen, bauen und konfigurieren das Spindelantrieb, Kühlung und Hilfsstromversorgung umfasst. Aufgrund der Komplexität des Projekts habe ich meine Arbeit in verschiedenen, aufeinander aufbauenden Phasen durchgeführt. Ich begann mit der Informationsphase, in der ich mir einige Fragen beantworten musste, die ich im Folgenden erläutere.&lt;/p&gt;&#10;&lt;h3 id="antrieb-mit-variabler-frequenz"&gt;Antrieb mit variabler Frequenz&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/spindleupgrade/switchbox.jpg" alt="Bild: Schaltkasten mit FU" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Wie ich in &lt;a href="https://blog.schallbert.de/cnc-electronics/#vfd"&gt;diesem Artikel&lt;/a&gt; bereits schrieb, wird ein Frequenzumrichter zur Steuerung der Drehzahl einer HF-Spindel verwendet. Der Motor heißt so, weil er weit oberhalb der Netzfrequenz (in meinem Kontinent 50 Hz) arbeitet, um höhere Drehzahlen zu erreichen wie sie für meine Anwendungen oft erforderlich sind.&lt;/p&gt;&#10;&lt;p&gt;Um einige Fragen in der Tiefe beantworten zu können, telefonierte ich mit einem Experten von &lt;a href="https://industrial.omron.de/de/products/mx2" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Omron&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; (Hersteller von Frequenzumrichtern), Herrn Wagner.&lt;/p&gt;&#10;&lt;h3 id="uf-kennlinie-oder-sensorlose-vektorregelung"&gt;U/f-Kennlinie&amp;quot; oder &amp;ldquo;sensorlose Vektorregelung&amp;rdquo;?&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Wird die U/f-Kennlinie gewählt, reduziert der Frequenzumrichter die Spannung zusammen mit der Frequenz, solange der Motor unterhalb seiner Eckfrequenz arbeitet (die bei meiner Maschine 400 Hz beträgt). Das liegt daran, dass die Induktivität der Ständerwicklung mit niedrigerer Frequenz abnimmt. Das wiederum erhöht den Stromfluss durch den Stator, und damit steigen auch Drehmoment und Verlustleistung. Durch die Anwendung der U/f-Kennlinie wird der Strom und damit das Drehmoment letztlich konstant gehalten. Bei sehr langsamen Drehzahlen nimmt die Induktivität im Vergleich zum Statorwiderstand jedoch so sehr ab, dass das Drehmoment überproportional abfällt. Daher ist diese Option für den Betrieb im unteren Drehzahlbereich eine schlechte Wahl. An oder oberhalb der Eckfrequenz des Motors hingegen bleibt die Spannung stabil und nur die Frequenz wird erhöht, was das Drehmoment verringert.&lt;/li&gt;&#10;&lt;li&gt;Bei der &amp;ldquo;sensorlosen Vektorregelung&amp;rdquo; verwendet der FU ein mathematisches Modell des Motors den er antreibt. So können optimale Schaltzeiten für die Treiber erreicht werden und damit kann unabhängig von der Motordrehzahl immer das Nenndrehmoment (oder sogar mehr) zur Verfügung gestellt werden. Dies ist besonders nützlich für Anwendungen, die einen Schweranlauf erfordern wie z. B. Aufzüge usw. Das Modell des Motors wird zumeist über eine automatische Kalibrierroutine ermittelt, wobei die Antriebsmaschine mit dem FU verbunden sein muss.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Hier ist ein Link zu einem &lt;a href="https://www.youtube.com/watch?v=qEM9VRSfXUo" target="_blank" rel="noopener noreferrer" class="external-link"&gt;youtube-Video&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, in dem die beiden Optionen bei niedrigen Drehzahlen verglichen werden. &lt;a href="https://www.youtube.com/watch?v=tWpChgsAIN4" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Ein weiteres Video&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zeigt, wie der FU bei großer Last sogar mehr als das Nenndrehmoment des Motors zur Verfügung stellen kann.&lt;/p&gt;&#10;&lt;p&gt;Da die meisten der von mir verwendeten Schaftfräser eine Drehzahl von &lt;code&gt;15000 U/min&lt;/code&gt; oder mehr erfordern, werde ich die Nachteile der U/f-Kennlinie wahrscheinlich nicht spüren. Außerdem ist bei meinem FU die Option &amp;ldquo;sensorlose Vektorregelung&amp;rdquo; nur bis &lt;code&gt;400 Hz&lt;/code&gt; verfügbar, so dass ich nur 80 % des Drehzahlbereichs der Spindel nutzen könnte. Daher entscheide ich mich für die U/f-Kennlinie.&lt;/p&gt;&#10;&lt;h3 id="auslauf-bremswiderstand-oder-energierückgewinnungssystem"&gt;Auslauf, Bremswiderstand oder Energierückgewinnungssystem?&lt;/h3&gt;&#10;&lt;p&gt;Zum Abbremsen des Motors kann der Frequenzumrichter mit verschiedenen Optionen konfiguriert werden. Wenn der Motor per Standardeinstellung im Freilauf ausläuft, kann es lange dauern, bis die Spindel bei voller Drehzahl zum Stillstand kommt.&lt;/p&gt;&#10;&lt;p&gt;Wenn ein Bremswiderstand konfiguriert wird wie ich es bei meiner Anwendung mache, benötigt das Abbremsen nur zwei Sekunden. Ich habe außerdem eine Gleichstrombremse konfiguriert, sodass die Welle vollständig zum Stillstand gebracht wird.&lt;/p&gt;&#10;&lt;p&gt;Für leistungsstärkere Maschinen oder Anwendungen, bei denen große Massen bewegt werden, kann ein Energierückgewinnungssystem sinnvoll sein. Es wird an den Zwischenkreis des FU angeschlossen und ist in der Lage, die durch die überschüssige Bewegungsenergie des Motors erzeugte Leistung ins Netz zurückzuspeisen. Um diese Funktion zu nutzen, ist ein zusätzliches Gerät erforderlich und die Kompatibilität des FU-Zwischenkreises mit diesem Einspeisewandler muss sichergestellt werden. Die hohen Systemkosten zahlen sich nur aus, wenn große Energiemengen zurückgewonnen werden können.&lt;/p&gt;&#10;&lt;h3 id="welche-modulationsfrequenz-bzw-taktfrequenz-sollte-ich-verwenden"&gt;Welche Modulationsfrequenz bzw. Taktfrequenz sollte ich verwenden?&lt;/h3&gt;&#10;&lt;p&gt;Die Takt-/Modulations-/Trägerfrequenz ist die Häufigkeit, mit der die Leistungstransistoren im FU schalten, um eine sinusähnliche Ausgangsspannung für den Motor zu liefern. Bei meinem FU kann sie in einem Bereich von &lt;code&gt;2-15kHz&lt;/code&gt; konfiguriert werden.&lt;/p&gt;&#10;&lt;p&gt;Um eine optimale Frequenz zu wählen, muss ein Gleichgewicht zwischen mehreren Faktoren gefunden werden:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Die Schaltverluste der Transistoren steigen bei höheren Frequenzen.&lt;/li&gt;&#10;&lt;li&gt;Die Durchlassverluste von Transistoren nehmen bei niedrigeren Frequenzen zu.&lt;/li&gt;&#10;&lt;li&gt;Das &lt;a href="https://en.wikipedia.org/wiki/Equal-loudness_contour" target="_blank" rel="noopener noreferrer" class="external-link"&gt;menschliche Ohr ist besonders empfindlich&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; im Bereich von &lt;code&gt;2-5kHz&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Resonanzfrequenzen des Motorsystems sollten vermieden werden.&lt;/li&gt;&#10;&lt;li&gt;Elektromagnetische Störaussendungen nehmen bei höheren Frequenzen zu.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Wenn der Motorhersteller eine Empfehlung für die Modulationsfrequenz abgibt, sollten diese natürlich befolgt werden. Ich habe mit verschiedenen Werten herumgespielt und mich für &lt;code&gt;5kHz&lt;/code&gt; als guten Kompromiss zwischen den oben genannten Faktoren entschieden. Ich hatte auch den Eindruck, dass dieser Wert bei meiner Maschine besser klang als 6 oder 4 kHz.&lt;/p&gt;&#10;&lt;h3 id="kann-ich-die-luftkühlung-in-abhängigkeit-von-der-spindeltemperatur-steuern"&gt;Kann ich die Luftkühlung in Abhängigkeit von der Spindeltemperatur steuern?&lt;/h3&gt;&#10;&lt;p&gt;Ja, das ist möglich. Alles, was dafür benötigt wird ist ein kontinuierlicher (nicht schaltender) Temperatursensor, z. B. ein &lt;code&gt;PT100&lt;/code&gt; oder ein anderes Thermoelement, ein Messwertgeber und ein freier Analogeingang am Frequenzumrichter. Dieser wird so konfiguriert, dass er über eine Fensterfunktion einen der freien digitalen Ausgänge ansteuert, der wiederum ein Relais schaltet, an das der Lüfter angeschlossen werden muss.&lt;/p&gt;&#10;&lt;p&gt;Meine Spindel ist leider mit einem schaltenden Thermistor ausgestattet, der über einen Temperaturalarm-Eingang mit dem FU verbunden ist. So kann er das System bei Überhitzung abschalten. Er ist aber nicht für eine kontinuierliche Temperaturüberwachung/-steuerung des Lüfters geeignet.&lt;/p&gt;&#10;&lt;h3 id="reicht-eine-spannung-von-18-v-aus-um-an-einem-24-v-digitaleingang-high-signalpegel-zu-erkennen"&gt;Reicht eine Spannung von 18 V aus, um an einem 24 V-Digitaleingang &amp;ldquo;High&amp;rdquo;-Signalpegel zu erkennen?&lt;/h3&gt;&#10;&lt;p&gt;Ja, bei meinem FU konnte ich eine niedrigere Spannung als die angestrebten 24 V an einen Logikeingang anlegen und trotzdem wurde der Pegel &lt;code&gt;HIGH&lt;/code&gt; erkannt.&lt;/p&gt;&#10;&lt;h3 id="kann-ich-den-logischen-masseanschluss-des-fu-ohne-probleme-an-die-masse-meiner-gleichstromversorgung-anschließen"&gt;Kann ich den logischen Masseanschluss des FU ohne Probleme an die Masse meiner Gleichstromversorgung anschließen?&lt;/h3&gt;&#10;&lt;p&gt;Ja. Muss man sogar. Der &amp;lsquo;Run&amp;rsquo;-Ausgang meiner CNC ist potenzialfrei. So musste ich die positive Steuerspannung des FU auf den Kontakt legen und dessen Ausgang mit dem &amp;lsquo;Run&amp;rsquo;-Eingang des FU verbinden. Der Stromkreis ist aber nur dann geschlossen, wenn im FU-Schaltschrank Logik-Masse und DC-Masse verbunden werden. Auch die analoge Ausgangsspannung meiner CNC zur Steuerung der Spindeldrehzahl muss dieselbe Erdungsreferenz wie der FU verwenden, also ist sie ebenfalls mit der logischen Masse des FU verbunden. Der Schaltplan zeigt diesen Sachverhalt an der &lt;a href="https://blog.schallbert.de/projects/spindle-upgrade/#die-planungsphase"&gt;Masseverbindung von T1&lt;/a&gt;&lt;/p&gt;&#10;&lt;h3 id="was-muss-ich-hinsichtlich-der-elektromagnetischen-verträglichkeit-beachten"&gt;Was muss ich hinsichtlich der elektromagnetischen Verträglichkeit beachten?&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Netzfilter zwischen Netzschalter und FU verwenden (Das ist in Deutschland sogar Vorschrift).&lt;/li&gt;&#10;&lt;li&gt;Abgeschirmte Kabel für den angeschlossenen Motor verwenden.&lt;/li&gt;&#10;&lt;li&gt;Die Motoranschlussleitung so kurz wie möglich halten.&lt;/li&gt;&#10;&lt;li&gt;Müssen doch lange Leitungen verwenden (&amp;gt;10m), HF-Rauschfilter bzw. Drossel einbauen.&lt;/li&gt;&#10;&lt;li&gt;Abschirmung zusammen mit den Schutzerdungsleitungen des Motors an die Schutzerdungsplatte des FU anschließen.&lt;/li&gt;&#10;&lt;li&gt;Sicherstellen, dass die Abschirmung einen leitenden Kontakt zu den Kabelsteckern herstellt.&lt;/li&gt;&#10;&lt;li&gt;Durchgangsprüfung nach DIN VDE 0701-0702 durchführen.&lt;/li&gt;&#10;&lt;li&gt;Vergewissern, dass der Schaltkasten über eine zentrale Schutzerdungsklemme verfügt, an die jedes Gerät innerhalb des Schaltkastens angeschlossen wird.&lt;/li&gt;&#10;&lt;li&gt;Vergewissern, dass die Schutzerde der Netzversorgung ebenfalls an diese Klemme angeschlossen ist.&lt;/li&gt;&#10;&lt;li&gt;Bei der Verkabelung des Schaltkastens die Motorzuleitung so weit wie möglich von den Signalleitungen entfernt verlegen.&lt;/li&gt;&#10;&lt;li&gt;Zur Reduktion elektromagnetischer Störabstrahlung sind Schaltkastengehäuse aus Metall solchen aus Kunststoff vorzuziehen. Zumindest ist eine metallische Trägerplatte zu verwenden, auf der alle Komponenten montiert werden.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="spindel"&gt;Spindel&lt;/h3&gt;&#10;&lt;p&gt;Um die passende Spindel für Ihre Maschine und Anwendung auszuwählen, gibt es - unabhängig vom Motortyp - einige Punkte zu beachten.&lt;/p&gt;&#10;&lt;h3 id="leistung"&gt;Leistung&lt;/h3&gt;&#10;&lt;p&gt;Die Spindelleistung ist vielleicht weniger wichtig als gedacht. Hier mache ich ein paar Beispiele zur Orientierung.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;&amp;lt;=400W&lt;/code&gt; HF-Spindel oder &lt;code&gt;&amp;lt;=800W&lt;/code&gt; Oberfräsenmotor: Für eine Hobby-Maschine, die nicht unbedingt mit hohen Vorschüben betrieben werden kann. Bei schwerer zu schneidenden Materialien wie Aluminium oder HPL reicht eine geringe oder mittlere Schnitttiefe pro Durchgang aus. Der Maschinenrahmen wiegt weniger als 60 kg und ist für sein Gewicht relativ groß, z.B. bei einer Arbeitsfläche von &lt;code&gt;&amp;gt;0,5 m²&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;&amp;lt;=700W&lt;/code&gt; HF-Spindel oder &lt;code&gt;&amp;lt;=1200W&lt;/code&gt; Oberfräsenmotor: Die Maschine wiegt mehr oder ist steifer als die oben genannte Option. Häufig werden Fräser mit einem Durchmesser um die 6mm verwendet oder es muss viel mit Planfräsern gearbeitet werden. Die Z-Achse der Maschine ist kräftig ausgelegt und man hat das Gefühl, dass die Maschine bei &lt;code&gt;3000mm/min&lt;/code&gt;, &lt;code&gt;Z+6mm&lt;/code&gt; und einem 6mm Fräser locker mit Hartholz zurechtkommt.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;1,1kW/1,5kW&lt;/code&gt; HF-Spindel: Eine kompakte, schwerere Maschine (ca. 500x500mm Verfahrweg bei 100kg Gewicht), die Aluminium oder Holz mit höheren Vorschubgeschwindigkeiten schneiden soll. Bei der Bearbeitung von Holz verwenden Sie häufig Schruppfräser, um Zeit zu sparen. Die Maschine kann höhere Beschleunigungen und schnelle Bewegungen bewältigen (z.B. &lt;code&gt;1000mm/s²&lt;/code&gt; und &lt;code&gt;300mm/s&lt;/code&gt;).&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;&amp;gt;=2,2kW&lt;/code&gt;: Sie haben den Hobbybereich verlassen - wahrscheinlich für immer - sowohl mit Ihrer Maschine als auch mit dem Auftragsbestand, der sich auf Ihrem Schreibtisch angesammelt hat. Sie haben eine Werkstatt, eine professionelle Staubabsaugung und eine schwerere Maschine, der Sie zutrauen, den ganzen Tag lang ohne intensive Überwachung zu laufen. Vielleicht betrachten Sie diesen Blog als &amp;ldquo;nicht anspruchsvoll genug&amp;rdquo; und machen sich auf zu neuen Ufern, wo man Ihnen als Profi wirklich helfen kann.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="gewicht"&gt;Gewicht&lt;/h3&gt;&#10;&lt;p&gt;Das Gewicht hängt von der Leistung ab. Ein 800-Watt-Motor für eine Oberfräse kann einschließlich Halterung unter 2 kg wiegen, während eine 2,2-kW-HF-Spindel mit automatischem Werkzeugwechsel ohne Werkzeughalter 6,5 kg oder mehr auf die Waage bringt. Auch Halterung und Leitungen werden tendenziell größer.&#10;Ein hohes Spindelgewicht kann sich vor allem bei leichten Maschinen negativ auswirken. Im Extremfall kann sich die Z-Achse nach unten bewegen, wenn die Schrittmotortreiber inaktiv sind. Die Maschinenbeschleunigung, insbesondere an der Portalbewegungsachse, muss möglicherweise reduziert werden, damit keine Schritte verloren gehen. Es ist zu bedenken, dass schwerere Spindeln in der Regel voluminöser sind, so dass sich die Schnittkräfte aufgrund einer größeren Hebelwirkung ungünstig auf die Z-Achse auswirken können.&#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/spindleupgrade/mafellvsspinogy.jpg" alt="Bild: Vergleich von Routermotor und HF-Spindel" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&lt;/p&gt;&#10;&lt;h3 id="2-polige-oder-4-polige-ausführung"&gt;2-polige oder 4-polige Ausführung?&lt;/h3&gt;&#10;&lt;p&gt;Wird insbesondere bei niedrigen Drehzahlen ein hohes Drehmoment benötigt, sollten Sie sich für eine 4-polige Ausführung entscheiden. Dann wird allerdings die doppelte Frequenz benötigt, um die gleiche Drehzahl wie bei einem 2-poligen Motor zu erreichen.&#10;Wird die Maschine häufig mit hohen Drehzahlen betrieben, sollte stattdessen eine 2-polige Spindel Anwendung finden.&lt;/p&gt;&#10;&lt;h3 id="wasser--oder-luftgekühlt"&gt;Wasser- oder luftgekühlt?&lt;/h3&gt;&#10;&lt;p&gt;Auch dies ist eher eine Frage der Kosten und der Professionalität als alles andere. Natürlich funktioniert die Selbstkühlung durch einen auf der Welle fest montierten Lüfter gut. Eine Fremdkühlung per Lüfter ist etwas teurer, aber auch leiser. Ihr Hauptvorteil besteht darin, dass die Spindel unabhängig von ihrer Drehzahl nicht zu warm wird.&lt;/p&gt;&#10;&lt;p&gt;Eine Wasserkühlung führt die Wärme nochmal deutlich effizienter aus dem Motor ab, und die hohe Wärmekapazität des Wassers führt zu homogenen Temperaturen in der gesamten Spindel. Sie dämpft die Vibrationen und ist die leiseste Kühlungslösung von allen. Andererseits ist das System viel teurer, komplexer, benötigt mehr Platz für Pumpe, Kühler und Ausgleichsbehälter usw. und muss öfter gewartet werden.&lt;/p&gt;&#10;&lt;h2 id="die-planungsphase"&gt;Die Planungsphase&lt;/h2&gt;&#10;&lt;p&gt;Nachdem die Handbücher aller bestellten Teile überflogen waren, konnte ich einen schematischen Schaltplan für das Spindel-Subsystem zeichnen.&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/spindleupgrade/vfd_circuit_plans_1.jpg" alt="Bild: Schaltplan FU-Subsystem" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Oben ist der Frequenzumrichter mit seinen Klemmen zu sehen. &lt;code&gt;X1&lt;/code&gt; ist der Netzstromeingang mit dem Trennschalter &lt;code&gt;S2&lt;/code&gt;. &lt;code&gt;X2&lt;/code&gt; verbindet die Spindel mit dem FU und &lt;code&gt;X3&lt;/code&gt; ist der Signalanschluss für die CNC. Rechts unten befindet sich die Gleichstromversorgung &lt;code&gt;T1&lt;/code&gt;, die die Luftkühlung versorgt und die logische Spannungsschiene bereitstellt. Die Temperatur des Spindelmotors &lt;code&gt;M1&lt;/code&gt; wird durch den Thermistor &lt;code&gt;R3&lt;/code&gt; überwacht (hier habe ich vergessen, die Beschriftung hinzuzufügen). Der Spindelkühlmotor &lt;code&gt;M2&lt;/code&gt; wird durch ein Zeitrelais gesteuert, das wiederum durch das &amp;ldquo;RUN&amp;rdquo;-Signal gesteuert wird. Ein digitaler Mehrzweckausgang ist mit einer roten LED &lt;code&gt;D1&lt;/code&gt; verbunden, die so konfiguriert ist, dass sie aufleuchtet, wenn eine Überlastwarnung vorliegt.&lt;/p&gt;&#10;&lt;p&gt;Der Schaltplan ist ziemlich einfach gehalten. Deshalb habe ich keine Zeit in saubere Zeichnungen investiert. Um es noch authentischer aussehen zu lassen, habe ich etwas ☕ auf das Papier verschüttet.&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/spindleupgrade/vfd_circuit_plans_3.jpg" alt="Bild: Anschlussbelegung der Spindel" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Dies ist die Pinbelegung des Spindelsteckers &lt;code&gt;X2&lt;/code&gt; und des entsprechenden Anschlusses am FU. Sie war mehrmals nützlich, als ich dem abgeschirmten Motorkabel (orangefarben, siehe Bannerbild oben) Steckverbinder hinzufügte sowie während der Geräteprüfung.&lt;/p&gt;&#10;&lt;p&gt;Ich notiere mir stets das erforderliche Anzugsdrehmoment für jede Schraube, einschließlich der für die Klemmen, und lege die benötigten Werkzeuge vor Baubeginn bereit.&lt;/p&gt;&#10;&lt;h2 id="die-bauphase"&gt;Die Bauphase&lt;/h2&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&lt;iframe src="https://makertube.net/videos/embed/u8gDBQnsdxYFEZfFwk6jfU" frameborder="0" allowfullscreen&gt;&lt;/iframe&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Schaltschrankbau für den Umrichter der Spindel&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h2 id="inbetriebnahme"&gt;Inbetriebnahme&lt;/h2&gt;&#10;&lt;p&gt;Vor Inbetriebnahme war der &lt;a href="https://blog.schallbert.de/testing-hardware/#test-plan"&gt;Testplan&lt;/a&gt; abzuarbeiten. Danach war der Spindel-Fettverteilungslauf gemäß Anweisungen des Herstellers zu starten.&#10;Schließlich kalibrierte ich den Analogausgang &amp;ldquo;0-10V&amp;rdquo; in der CNC-Software, um die Frequenz des Frequenzumrichters mit der Drehzahl der Spindel abzugleichen. Beispiel: &lt;code&gt;S10000&lt;/code&gt; sollte &lt;code&gt;3,33V&lt;/code&gt; am Ausgang erzeugen und den FU mit einer Frequenz von etwa &lt;code&gt;175Hz&lt;/code&gt; laufen lassen, um den Schlupf zu berücksichtigen.&lt;/p&gt;&#10;&lt;p&gt;Um zu sehen, dass das System wie vorgesehen funktioniert, habe ich ein paar Testläufe gemacht: 8mm PMMA, Vollnut, einmaliger Durchlauf in &lt;code&gt;Z+=8mm&lt;/code&gt;, &lt;code&gt;4mm&lt;/code&gt; Einschneider (poliert) Hartmetallfräser bei &lt;code&gt;S=26kUPM&lt;/code&gt; und &lt;code&gt;F=3000mm/min&lt;/code&gt;.&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/spindleupgrade/spindle_commisioning_initjob.jpg" alt="Bild: Spindel mit einem Werkstück aus PMMA" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h2 id="die-analysephase"&gt;Die Analysephase&lt;/h2&gt;&#10;&lt;p&gt;Diese Phase wird später folgen, vielleicht ein Jahr nach der Arbeit mit der neuen Spindel. Ich werde mir Folgendes ansehen:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Hat sich die Investition gelohnt (etwa 8x teurer als ein Oberfräsenmotor)?&lt;/li&gt;&#10;&lt;li&gt;Habe ich die zusätzlichen Möglichkeiten (höhere Leistung und mehr Drehzahl) genutzt?&lt;/li&gt;&#10;&lt;li&gt;Erzielt sie spürbar bessere Ergebnisse?&lt;/li&gt;&#10;&lt;li&gt;Ist sie zuverlässiger und wartungsärmer als mein bisheriger Motor?&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Aber was ich schon jetzt sagen kann: Ich habe viel gelernt. Es hat Spaß gemacht, das System zu entwerfen, zu verkabeln und einzurichten. Und es war aufregend zu sehen, dass es tatsächlich so funktioniert, wie ich es mir vorgestellt habe 🙂.&lt;/p&gt;&#10;</description></item><item><title>Spirograph</title><link>https://blog.schallbert.de/projects/spirograph/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/spirograph/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/spirograph/spirograph-thumb.jpg"&#10; class="post-cover"&#10; alt="Bild: Spirograph aus Acrylglas, gefertigt auf meiner CNC &amp;#39;Zerspanobert&amp;#39;"&#10; title="Spirograph" /&gt;&#10;&lt;h2 id="der-spirograph"&gt;Der Spirograph&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://de.wikipedia.org/wiki/Spirograph" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Spirographen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; sind mathematische Spielzeuge. Mit ihrer Hilfe kann man ganz leicht sich wiederholende Kurven zeichnen. Die einfachste Version besteht aus einem Zahnrad, das auf der Innenseite eines feststehenden Zahnkranzes läuft. Das Zahnrad hat ein paar Löcher, in die die Spitze eines Stiftes passt. Die Löcher befinden sich außerhalb der Mitte des Zahnrads, so dass beim Drehen des Rads im Inneren des Hohlrads parabolische Formen entstehen.&lt;/p&gt;&#10;&lt;p&gt;Die Anzahl der Umdrehungen, die nötig sind um eine geschlossene Form zu zeichnen, hängt vom Verhältnis der Zahnanzahl zwischen den beiden Zahnrädern ab.&lt;/p&gt;&#10;&lt;h3 id="version-0"&gt;Version 0&lt;/h3&gt;&#10;&lt;p&gt;Der erste Entwurf haben wir gemeinsam mit den Jungs von CADasCAM &lt;a href="https://hobbyline.info/forum/index.php?thread/839-hobby-line-tage-27-28-5/&amp;amp;postID=9215#post9215" target="_blank" rel="noopener noreferrer" class="external-link"&gt;in der Werkhalle&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; erstellt. Es handelt sich um eine 14:9-Zahnkombination. Das kleinste gemeinsame Vielfache (auch KGV genannt) der beiden Zahnräder beträgt 126, was zu Zeichnungen mit vielen (&lt;code&gt;126 / 9 = 14&lt;/code&gt;) Kanten führt. Wir entschieden uns für Aluminium als Material.&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/spirograph/20230528_sorotecworkshop_spirograph_aluminium.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;Fräsen von Spirograph_V0 auf einer übertakteten CompactLine CNC&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Die Fertigung war erfolgreich und die Räder ließen sich von Zahn zu Zahn platzieren aber leider nicht drehen, weil der nächste Zahn des inneren Zahnrads nicht auf die Flanke, sondern auf den Kopf des Hohlradzahns traf. Die Krümmung des Hohlrads hatten wir nämlich nicht berücksichtigt, sodass die langen Zahnköpfe zu eng zusammen standen.&lt;/p&gt;&#10;&lt;h3 id="version-1"&gt;Version 1&lt;/h3&gt;&#10;&lt;p&gt;Als der Workshop vorbei war, nahm ich den Entwurf mit nach Hause und versuchte ihn zu verbessern. Ich kürzte die Zahnköpfe des Hohlrads, um das vorherige Problem zu beheben und fügte Verrundungen hinzu, damit die Zahnräder nicht so schwergängig ineinander griffen.&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/spirograph/V1_upper.jpg" alt="Bild: Spirograph in Buche, Oberseite fertig gefräst" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Ich habe den Entwurf in &lt;code&gt;18mm&lt;/code&gt; Buche Leimholz gefräst. Ich verwende eine zweiseitige Bearbeitung, weil sich das Anfasen von Hand auf der Rückseite bei diesen komplexen Formen als etwas mühsam erwies. Dazu schneide ich nicht komplett durch das Material, sondern lasse unten eine &lt;code&gt;1mm&lt;/code&gt; dicke &amp;ldquo;Zwiebelhaut&amp;rdquo; stehen, die alles an ihrem Platz hält für den zweiten, rückseitigen Bearbeitungsvorgang.&lt;/p&gt;&#10;&lt;p&gt;Danach wird das Werkstück gedreht, sodass die Unterseite oben ist. So können die Kanten der Unterseite abgeschrägt werden. Dabei durchbohrt der Fräser die &amp;ldquo;Zwiebelhaut&amp;rdquo; und schneidet die Werkstücke praktisch aus dem Rohmaterial heraus, sodass ich sie nur noch vom Maschinenbett nehmen muss. So waren nur die Grate abzuschleifen, was weniger als fünf Minuten manuelle Nacharbeit erforderte.&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/spirograph/V1_detail.jpg" alt="Bild: Spirograph_V1 in Buche, Detailansicht der Zahnräder, die nicht passen wollten" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Aber es passte immer noch nicht. Diesmal, weil die Flanken des inneren Rades zu breit waren. Das hatte zur Folge, dass sie nicht die volle Eingriffstiefe zuließen was wiederum dazu führte, dass der nächste Zahn des Innenrads erneut auf den Zahnkopf des Zahnkranzes traf.&lt;/p&gt;&#10;&lt;h3 id="version-2"&gt;Version 2&lt;/h3&gt;&#10;&lt;p&gt;Ich habe herausgefunden, dass in Version_1 das kleine Zahnrad eine positiven &amp;ldquo;Zahnprofilverschiebung&amp;rdquo; hatte, während der Innenkranz auf Null belassen wurde. Das konnte überhaupt nicht funktionieren, also habe ich es korrigiert. Ich habe auch die konkaven Radien beider Räder größer geplant, sodass ein 6mm-Schaftfräser in der Lage sein würde, alle Details richtig herauszuarbeiten. Schließlich fügte ich etwas Spiel für die Zahnflanken hinzu und sorgte für mehr Platz am Zahnfuß, damit die Zähne besser ineinander greifen.&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/spirograph/V2_pmma.jpg" alt="Bild: Spirograph_V2 in PMMA, zusammen mit Beispielzeichnungen" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Um dem inneren Rad mehr Platz zu geben, habe ich die Zahnradkombination auf 16/7 verändert, was dann &lt;code&gt;112 / 7 = 16&lt;/code&gt; Schleifen zeichnen wird (noch engere Darstellung), bevor sich das Muster wiederholt.&lt;/p&gt;&#10;&lt;p&gt;Diesmal habe ich es aus 5mm extrudiertem Acryl (PMMA), opak weiß, gefertigt. Ich wählte eine ähnliche zweiseitige Frässtrategie wie oben beschrieben, fügte aber einen Schlichtlauf hinzu, um die Kantenqualität zu verbessern.&#10;Folgende Parameter kamen zur Verwendung:&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;Fräser, Material: PMMA (Acryl)&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Werkzeugtyp&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Zahn&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Durchmesser [mm]&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Drehzahl [U/min]&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Vorschub [mm/min]&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Z+ [mm]&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;XY+ [mm]&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;VHM-Habichtschnabel FSECO1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Schruppen&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;4&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;24000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;3000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;8&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;3,6&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;FSAC Acrylschneider poliert&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Schlichten&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;4&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;26000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;3000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;8&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0,2&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;Hartmetall Kegel 90° FEF&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Fasen&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;3&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;10&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;15000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;3&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;3&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;Und es funktionierte einwandfrei 😄. Die Schnittqualität war sehr gut. An manueller Nacharbeit blieb mir weniger als eine Minute Arbeit zum Entgraten.&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/spirograph/spirograph_V2_teethengaged.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;Aktualisierte Zahnparameter ermöglichen reibungslosen Eingriff&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Aber es gab noch Raum für Verbesserungen:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Löcher direkt in den Zähnen machen keinen Sinn, weil sich das innere Rad bei Eingriff dieses Zahns nicht mehr durch den Stift weiterdrehen lässt&lt;/li&gt;&#10;&lt;li&gt;5mm Löcher sind etwas zu klein, um alle Arten von Buntstiften unterzubringen&lt;/li&gt;&#10;&lt;li&gt;16 Zähne &lt;code&gt;modul10&lt;/code&gt; sind nicht genug, um ein DIN A4-Papier zu füllen&lt;/li&gt;&#10;&lt;li&gt;Die elliptische Form sieht schön aus, ist aber etwas schwer festzuhalten&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Deshalb habe ich einen weiteren Schritt in der Entwicklung gemacht:&lt;/p&gt;&#10;&lt;h3 id="version-3"&gt;Version 3&lt;/h3&gt;&#10;&lt;p&gt;Eine kleine Aktualisierung gegenüber Version_2, dieses Mal mit einer 18/10-Paarung (KGV von 90, also 9 Schleifen pro Durchlauf). Ich habe die Lochabstände in Form einer &lt;a href="https://de.wikipedia.org/wiki/Fibonacci-Folge" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Fibonacci&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;-Spirale überarbeitet, um ausdrucksstärkere Designs zu erzeugen. Schließlich habe ich den Umriss an die Größe von DIN A4-Papier angepasst, so dass die Entwürfe nun zentriert sind.&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/spirograph/V3_design.jpg" alt="Bild: Spirograph_V3 Entwurf" /&gt;&lt;/figure&gt;&#10;&lt;/div&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/spirograph/V3_detail.jpg" alt="Bild: Spirograph_V3 in PMMA, Detailaufnahme eines Zahns" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Für die Herstellung habe ich dieselben Parameter wie bei der vorherigen Version verwendet. Wie gut das Ergebnis geworden ist! Ich frage mich, ob ich die letzten 5% an Qualität herausholen kann, indem ich die Grate ganz vermeide. Der Grat entsteht, wenn der konische Fräser die Unterseite des Materials durchsticht, um den endgültigen Ausschnitt auf meinem Vakuumtisch herzustellen. Ich muss also in Zukunft etwas &amp;ldquo;tiefer&amp;rdquo; fräsen als das verbliebene Material stark ist. Der Hersteller der Fräser empfahl mir im Nachgang, Vorschub und Drehzahl bei Arbeit in Acrylglas für die Fasenfräser zu senken, wenn der Zielvorschub wegen ständiger Beschleunigungs- und Abbremsvorgänge nicht erreicht werden kann.&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/spirograph/V3_chattermarks_acceleration.jpg" alt="Bild: Spirograph_V3, Kontur-Detailansicht" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Außerdem ist mir aufgefallen, dass die Maschine Rattermarken erzeugt, wenn sie bei &amp;ldquo;G01&amp;rdquo;-Bearbeitungen auf die Zielgeschwindigkeit beschleunigt. Ich vermute, dass dies auf die begrenzte Steifigkeit des Rahmens zurückzuführen ist. Es ist nur unter einem bestimmten Winkel sichtbar, aber nicht spürbar.&lt;/p&gt;&#10;&lt;p&gt;Das fertige Produkt sieht dann so aus. Durch die Fasen und abgerundeten Kanten liegt es sehr angenehm in der Hand. Die Zahnräder laufen mühelos und die 18/10-Zahnradkombination hat so viele Zähne, dass auch Kinder kein Problem damit haben, die Formen herzustellen (ausprobiert).&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/spirograph/V3_pmma.jpg" alt="Bild: Spirograph_V3 in Acryl gefräst" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Und schließlich bin ich auch damit zufrieden, wie die Zeichnungen aussehen. Mit kräftigen Strichen und groß genug (15x15cm). Die Linien stehen nicht zu eng zusammen. Die höhere Anzahl von Löchern, die das größere innere Rad erlaubt, hilft auch dabei, eine größere Vielfalt an Figuren zu erzeugen.&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/spirograph/V3_drawing.jpg" alt="Bild: Zeichnungsbeispiel mit Spirograph_V3" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Ich habe die Versionen 2 sowie 3 einer nahegelegenen Schule sowie einem Kindergarten gespendet. Bin gespannt, wie sie im Alltag damit zurechtkommen.&lt;/p&gt;&#10;&lt;h2 id="lust-auf-einen-nachbau-bekommen"&gt;Lust auf einen Nachbau bekommen?&lt;/h2&gt;&#10;&lt;p&gt;Das ist kein Problem. &lt;a href="https://blog.schallbert.de/assets/docs/spirograph_V3_by_schallbert.zip"&gt;Hier steht ein &lt;code&gt;ZIP&lt;/code&gt;-Ordner zum Download bereit&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="was-ist-im-ordner-enthalten"&gt;Was ist im Ordner enthalten?&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://www.cadascam.com/de/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;CADasCAM&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Projekt, das alle Arten von Zahnradpaarungskonfigurationen ermöglicht&lt;/li&gt;&#10;&lt;li&gt;Zeichnungsdateien (DXF) für das Fräsen von Vorder- und Rückseite&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.estlcam.de/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;EstlCAM&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Projektdateien zusammen mit meiner Werkzeugdatenbank&lt;/li&gt;&#10;&lt;li&gt;CNC-Betriebsdateien (&lt;code&gt;tap&lt;/code&gt;), die zu meiner Maschine passen&lt;/li&gt;&#10;&lt;li&gt;Ein Readme (&lt;code&gt;txt&lt;/code&gt;), das den XY-Nullpunkt und die erforderlichen Offsets definiert&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="kostet-es-etwas"&gt;Kostet es etwas?&lt;/h3&gt;&#10;&lt;p&gt;Nein. Beachten Sie, dass die von mir verwendete Lizenz das Paket zu &amp;ldquo;freier Software&amp;rdquo; macht. Bitte kontaktieren Sie mich jedoch, falls Sie die Entwürfe gewerblich verwenden möchten.&lt;/p&gt;&#10;&lt;h3 id="haftungsausschluss"&gt;Haftungsausschluss&lt;/h3&gt;&#10;&lt;p&gt;Wie üblich stehen die Dateien kostenlos zum Download zur Verfügung. Im Gegenzug übernehme ich keine Verantwortung oder Haftung für deren Inhalt.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;WARNUNG&lt;/strong&gt; ⚠️&lt;/p&gt;&#10;&lt;p&gt;Diese Dateien bieten oder ermöglichen die Ausgabe von G-Code-Anweisungen für echte Maschinen, die in der physischen Welt tatsächlich Dinge tun. Es könnten Code-Fehler oder Bugs enthalten sein, die möglicherweise zu Maschinenabstürzen oder Schlimmerem führen können. Ich übernehme keine Haftung für Arbeitsunfälle, Systemausfälle, Geräteausfälle, Produktionsausfälle, Ablaufstörungen oder andere negative Auswirkungen, die durch die von mir zur Verfügung gestellten Dateien verursacht werden könnten.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;HINWEIS&lt;/strong&gt; ℹ️&lt;/p&gt;&#10;&lt;p&gt;Es liegt in der Verantwortung des Maschinenbetreibers, den G-Code sorgfältig zu prüfen und sicherzustellen, dass er auf der jeweiligen Maschine wie vorgesehen funktioniert und keinen Schaden verursacht.&lt;/p&gt;&#10;&lt;h2 id="update"&gt;Update&lt;/h2&gt;&#10;&lt;p&gt;Ein paar weitere sehr schöne Spirographen habe ich Anfang 2024 für die Maker Faire angefertigt. Das Grundmaterial ist mitteldichte Faserplatte (MDF), auf welche ich eine Lage Olivenholzfurnier aufgebracht habe.&#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-03-25-spirograph.jpg" alt="Image: Spirograph under test" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&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><item><title>Umzug von Github Pages</title><link>https://blog.schallbert.de/projects/move-blog-to-own-server/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/projects/move-blog-to-own-server/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/movetoownserver/giteawebdashboard-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Gitea Dashboard, served on my machine, proxied by caddy"&#10; title="Umzug von Github Pages" /&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: 5-50€/Monat&lt;/li&gt;&#10;&lt;li&gt;Zeitaufwand: ~10h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;&#10;&lt;p&gt;Ich habe mich entschieden, &lt;a href="https://blog.schallbert.de/projects/thissite/"&gt;meine Website&lt;/a&gt; von &lt;a href="https://pages.github.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Github-Pages&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; auf meinen eigenen Server umzuziehen. Dies geschieht aus zwei Gründen: Zum einen möchte ich den Server in Deutschland stehen haben (Datenschutz) und zum anderen bietet mir Github Pages keine einfach zu handhabende Möglichkeit, meine Site zweisprachig anzubieten.&lt;/p&gt;&#10;&lt;p&gt;Zusätzlich kann ich ebenfalls ein paar lang gehegte Wünsche realisieren: Ein eigener mini-Dateiserver und eine Webseite für meinen kleinen Nebenerwerb werden jetzt möglich. Aufsetzen werde ich sie allerdings in einem späteren Schritt.&lt;/p&gt;&#10;&lt;h2 id="überblick"&gt;Überblick&lt;/h2&gt;&#10;&lt;p&gt;In diesem Projekt werde ich einen virtuellen Server anmieten und dort per &lt;code&gt;SSH&lt;/code&gt; das Container-Managementsystem &lt;a href="https://www.docker.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Docker&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; installieren.&lt;/p&gt;&#10;&lt;p&gt;In einem Container soll eine Instanz von &lt;a href="https://docs.gitea.com/next/installation/install-with-docker#startup" target="_blank" rel="noopener noreferrer" class="external-link"&gt;gitea&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; laufen, welche das Versionsmanagement für mich übernehmen und über sogenannte Actions eine CI/CD-Pipeline&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; bereitstellen wird.&lt;/p&gt;&#10;&lt;p&gt;Durch die Automation mit &lt;a href="https://docs.gitea.com/next/usage/actions/overview" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Gitea Actions&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; möchte ich dann in der Lage sein, die von &lt;code&gt;Jekyll&lt;/code&gt; per &lt;a href="https://docs.gitea.com/next/usage/actions/quickstart#use-actions" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Workflow-Datei&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; gebaute Site auf einem &lt;code&gt;caddy&lt;/code&gt;-Server zu veröffentlichen, also in &amp;ldquo;Produktion&amp;rdquo; zu geben.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://caddyserver.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;caddy&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; stellt hier nicht nur den Server, sondern auch einen &lt;a href="https://en.wikipedia.org/wiki/Reverse_proxy" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Reverse Proxy&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zur Verfügung, welcher mir Portmapping und die Adresszuordnung abnehmen kann. Ganz nebenbei kümmert sich &lt;code&gt;caddy&lt;/code&gt; noch vollautomatisiert um die Zertifikatsverwaltung für &lt;code&gt;https&lt;/code&gt; über &lt;a href="https://letsencrypt.org/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Let&amp;rsquo;s Encrypt&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Da auch caddy in Docker laufen wird, müssen virtuelle Netzwerke angelegt und korrekt konfiguriert werden, sodass die Container miteinander &amp;ldquo;reden&amp;rdquo; können.&lt;/p&gt;&#10;&lt;p&gt;Schreiten wir zur Tat - aber immer Schritt für Schritt.&lt;/p&gt;&#10;&lt;h2 id="konfiguration-meines-servers"&gt;Konfiguration meines Servers&lt;/h2&gt;&#10;&lt;p&gt;Die folgenden Abschnitte beschreiben Schritt für Schritt, wie ich vorgegangen bin, meinen Cloud-Server einzurichten und für das Veröffentlichen dieses Blogs vorzubereiten.&lt;/p&gt;&#10;&lt;h3 id="ssh-auf-dem-server-einrichten"&gt;SSH auf dem Server einrichten&lt;/h3&gt;&#10;&lt;p&gt;Direkt nachdem ich den sogenannten &amp;ldquo;Cloud-Server&amp;rdquo; angemietet hatte, habe ich den Zugriff per SSH konfiguriert und damit den Passwort-Login deaktiviert.&#10;Dafür prüfe ich zuerst, ob mein &lt;code&gt;ssh-agent&lt;/code&gt; bereits läuft bzw. starte ihn direkt mit dem Befehl &lt;code&gt;eval ssh-agent&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Sodann erstelle ich mir ein neues Schlüsselpaar per Befehl&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;ssh-keygen -t ed25519 -C &amp;quot;your_email@example.com&amp;quot; -f &amp;quot;~/.ssh/mein-cloud-server&amp;quot;&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;Dieses Beispiel erzeugt Schlüsseldateien mit dem Verfahren (&lt;code&gt;-t&lt;/code&gt;) &lt;code&gt;ed25519&lt;/code&gt;, welche per Kommentar (&lt;code&gt;-C&lt;/code&gt;) eine Mailadresse enthalten und unter dem Dateinamen (&lt;code&gt;-f&lt;/code&gt;) &lt;code&gt;mein-cloud-server&lt;/code&gt; gespeichert werden.&lt;/p&gt;&#10;&lt;p&gt;Den öffentlichen Schlüssel &lt;code&gt;mein-cloud-server.pub&lt;/code&gt; habe ich dann im Webportal meines Servers unter &amp;ldquo;Security&amp;rdquo; eingegeben und den dann erhaltenen Fingerprint verifiziert mittels&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;ssh-keygen -lf &amp;lt;fingerprint_from_server&amp;gt; &amp;quot;~/.ssh/mein-cloud-server&amp;quot;&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;Anschließend habe ich die &lt;code&gt;config&lt;/code&gt;-Datei im &lt;code&gt;.ssh&lt;/code&gt;-Verzeichnis so angepasst, dass ich nur noch &lt;code&gt;ssh server&lt;/code&gt; statt der IP-Adresse eingeben muss, um mich drauf zu verbinden - Faulheit siegt 😀&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-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#.ssh/config&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Host server&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;User root&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Hostname ip_address_of_server&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;PreferredAuthentications publickey&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;IdentityFile ~/.ssh/mein-cloud-server&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Gebe ich nun &lt;code&gt;ssh server&lt;/code&gt; in die Konsole ein, so erscheint beim ersten Mal die Meldung, dass der Verbindungsaufbau zu einem unbekannten Partner mit Fingerprint soundso stattfinden wird und bittet um explizite Zustimmung. Nun kann der Fingerprint mit dem des Servers (auf der Webseite des Anbieters) abgeglichen und somit sichergestellt werden, dass man auch wirklich zu dem angestrebten, eigenen Server verbindet und nicht zu einem Dritten, der die Anfragen lediglich transparent weiterleitet. Stimmt man mit &lt;code&gt;yes&lt;/code&gt; zu, so wird der Fingerprint der &lt;code&gt;known_hosts&lt;/code&gt;-Datei hinzugefügt und das Zielsystem in Zukunft als bekannt angesehen.&lt;/p&gt;&#10;&lt;h2 id="docker-installieren-"&gt;Docker installieren 🐳&lt;/h2&gt;&#10;&lt;p&gt;Auf meinem Server läuft ein Standard-Ubuntu, für die die von Docker bereitgestellte &lt;a href="https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Anleitung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zur Installation des Docker-Repository per &lt;code&gt;apt&lt;/code&gt; reibungslos funktioniert. Einfach per SSH drauf einwählen und die entsprechenden Befehle ausführen - fertig. Dabei wird der von mir benutzte Dienst &lt;code&gt;docker-compose&lt;/code&gt; gleich mit installiert, welcher Konfigurationsdateien für die Zielcontainer interpretieren und die Container mit den entsprechenden Parametern gleich richtig hoch- und herunterfahren kann.&lt;/p&gt;&#10;&lt;h2 id="caddy"&gt;caddy installieren 🛒&lt;/h2&gt;&#10;&lt;p&gt;Zuerst lege ich den Ordner per &lt;code&gt;mkdir caddy2&lt;/code&gt; im Stammverzeichnis meines Servers an - soviel Zeit muss sein. Um caddy im Docker laufen lassen zu können, habe ich in diesem Ordner die folgende &lt;code&gt;docker-compose.yml&lt;/code&gt; erstellt:&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-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#caddy2/docker-compose.yml&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:#f92672"&gt;version&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;3&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:#f92672"&gt;services&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;caddy&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;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;caddy:latest&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;container_name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;caddy&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;ports&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#e6db74"&gt;&amp;#34;80:80&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:#e6db74"&gt;&amp;#34;443:443&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:#f92672"&gt;environment&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;ACME_AGREE=true&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;restart&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;unless-stopped&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;volumes&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;./Caddyfile:/etc/caddy/Caddyfile&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;./caddy_data:/data&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;./caddy_config:/config&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;./www:/www&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;networks&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;caddy-proxy&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:#f92672"&gt;networks&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;caddy-proxy&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;external&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Diese Datei sagt Docker, dass es für den Service das aktuellste Image von &lt;code&gt;caddy&lt;/code&gt; verwenden und dem Container den Namen &lt;code&gt;caddy&lt;/code&gt; geben soll. Des Weiteren sollen die Ports &lt;code&gt;80&lt;/code&gt; (http) sowie &lt;code&gt;443&lt;/code&gt; (https) verfügbar gemacht und die Zertifikatsverwaltung per &lt;a href="https://www.howtogeek.com/devops/how-to-request-a-letsencrypt-certificate-using-acme/#installing-acme-sh" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;code&gt;ACME_AGREE&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zugelassen werden. Der Container bindet die von Caddy benötigten Dateien (Caddyfile, data, config) ein und kann ebenso auf die von mir unter &lt;code&gt;/www&lt;/code&gt; abgelegten Dateien zum späteren Veröffentlichen meiner Website zugreifen. Der Container befindet sich im Netzwerk &lt;code&gt;caddy-proxy&lt;/code&gt; und durch &lt;code&gt;external: true&lt;/code&gt; weiß der Container, dass dieses Netzwerk unabhängig von diesem Container bereits besteht.&lt;/p&gt;&#10;&lt;p&gt;Damit wir den caddy-Container korrekt aufsetzen können, müssen wir also das Docker-Netzwerk vorher per &lt;code&gt;docker network create caddy-proxy&lt;/code&gt; erstellen. Mit &lt;code&gt;docker network ls&lt;/code&gt; kann man nun prüfen, ob das Netzwerk korrekt angelegt wurde.&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;NETWORK ID NAME DRIVER SCOPE&#10;&amp;lt;redacted&amp;gt; bridge bridge local&#10;&amp;lt;redacted&amp;gt; caddy2_default bridge local&#10;&amp;lt;redacted&amp;gt; caddy-proxy bridge local&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Sieht gut aus!&lt;/p&gt;&#10;&lt;p&gt;Anschließend erstelle ich eine &lt;code&gt;Caddyfile&lt;/code&gt; zur Konfiguration von Caddy:&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-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# caddy2/Caddyfile&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;blog.schallbert.de {&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;root * /www/blog&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;encode gzip&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;file_server&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dieser Eintrag sagt caddy, dass der Inhalt des Ordners &lt;code&gt;/www/blog&lt;/code&gt; als Dateiserver, der die &lt;code&gt;gzip&lt;/code&gt;-Komprimierung unterstützt, unter &lt;a href="https://blog.schallbert.de/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;blog.schallbert.de&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; verfügbar machen soll.&lt;/p&gt;&#10;&lt;h3 id="test-website-erstellen-"&gt;Test-Website erstellen 🧪&lt;/h3&gt;&#10;&lt;p&gt;Ach ja, was auch noch wichtig ist, bevor wir den Container starten: Der DNS&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; sollte die Adresse auch auflösen können. Dafür logge ich mich in meinem DNS-Manager ein und ordne der Subdomain die feste IP-Adresse meines Servers zu:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;TYPE NAME VALUE TTL&#10;A blog.schallbert.de &amp;lt;redacted&amp;gt; 86400&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Schlussendlich soll der Server ja auch etwas anzeigen können. Eine Testseite genügt ja schon. Also ganz fix:&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mkdir ~/caddy2/www/blog&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cd ~/caddy2/www/blog&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nano index.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Und dort dann ein unverkennbares Hello World eintragen:&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-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&amp;lt;!-- blog/index.html --&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;h1&lt;/span&gt;&amp;gt;ALL YOUR BASE ARE BELONG TO US&amp;lt;/&lt;span style="color:#f92672"&gt;h1&lt;/span&gt;&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Speichern, fertig.&lt;/p&gt;&#10;&lt;p&gt;Das Stammverzeichnis von caddy sieht nun so aus:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;*caddy_config* *caddy_data* Caddyfile docker-compose.yml *www*&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Jetzt endlich kann der Container erstellt und caddy darin hochgefahren 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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker compose up -d&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Die Option &lt;code&gt;-d&lt;/code&gt; bedeutet &amp;ldquo;detach&amp;rdquo;, also wird der Container hochgefahren und von der Konsole abgekoppelt, sodass sie nicht blockiert wird.&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;[+] Running 1/1&#10; ✔ Container caddy Started&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Super, das sieht auch gut aus. Jetzt schnell mal auf die Seite navigieren:&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/movetoownserver/address.jpg" alt="Image: caddy is displaying the test page correctly" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h2 id="gitea-installieren-"&gt;Gitea installieren 🍵&lt;/h2&gt;&#10;&lt;p&gt;Wie oben kurz angerissen, benötige ich Gitea für zwei Zwecke: Erstens möchte ich für meine Dateien wie von Github gewohnt ein Versionsmanagement haben und zweitens möchte ich meine Seite mit Hilfe von Gitea über Jekyll bauen und dann auf den Server schieben.&lt;/p&gt;&#10;&lt;p&gt;Wie schon bei caddy, beginnt alles in einem entsprechenden Ordner:&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cd ~&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mkdir gitea&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cd gitea&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nano docker-compose.yml&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Die &lt;code&gt;docker-compose.yml&lt;/code&gt; für Gitea habe ich mir aus verschiedenen &lt;a href="https://docs.gitea.com/installation/install-with-docker" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Beispielen zusammengebastelt&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Hier ist sie:&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-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# gitea/docker-compose.yml&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;version&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;3&amp;#34;&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:#f92672"&gt;networks&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;gitea&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;external&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&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;caddy-proxy&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;external&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:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;caddy-proxy&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:#f92672"&gt;services&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;gitea&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;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gitea/gitea:latest&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;container_name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gitea&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;restart&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;always&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;environment&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;USER_UID=1000&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;USER_GID=1000&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;SSH_DOMAIN=git.schallbert.de&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;GITEA_HOSTNAME=git.schallbert.de&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;networks&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;gitea&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;caddy-proxy&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;volumes&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;./gitea:/data&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;./log:/app/gitea/log&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;/etc/timezone:/etc/timezone:ro&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;/etc/localtime:/etc/localtime:ro&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;ports&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#e6db74"&gt;&amp;#34;127.0.0.1:3000:3000&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:#e6db74"&gt;&amp;#34;222:22&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:#f92672"&gt;runner&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;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gitea/act_runner:nightly&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;environment&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;CONFIG_FILE=/config.yml&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;GITEA_INSTANCE_URL=https://git.schallbert.de&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;GITEA_RUNNER_NAME=ichlaufe&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;GITEA_RUNNER_REGISTRATION_TOKEN=&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;volumes&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;./runner/config.yml:/config.yml&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;./runner/data:/data&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;/var/run/docker.sock:/var/run/docker.sock&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Die Bedeutung ist in etwa wie folgt: &lt;code&gt;gitea&lt;/code&gt; ist Teil der Docker-Netzwerke &lt;code&gt;gitea&lt;/code&gt; sowie &lt;code&gt;caddy-proxy&lt;/code&gt;. Der Container, in dem der Service läuft, heißt &lt;code&gt;gitea&lt;/code&gt; und wird bei Absturz stets automatisch neu gestartet. Die User-ID wird mit &lt;code&gt;1000&lt;/code&gt; in einem Wertebereich gesetzt, welcher weder für Administratoren noch andere besondere Benutzer reserviert ist. Per SSH und http ist gitea unter &lt;code&gt;git.schallbert.de&lt;/code&gt; erreichbar, und zwar auf Ports &lt;code&gt;22&lt;/code&gt; bzw. &lt;code&gt;3000&lt;/code&gt; - dort aber nur per localhost, also nicht &amp;ldquo;von außen&amp;rdquo;.&lt;/p&gt;&#10;&lt;p&gt;Gitea hat Zugriff auf Zeitzone und Lokalzeit des Servers, außerdem legt es Logs unter &lt;code&gt;/log&lt;/code&gt; und Dateien unter &lt;code&gt;/data&lt;/code&gt; ab. Gitea verfügt über einen &lt;code&gt;action runner&lt;/code&gt;, der auf der gitea-Instanz läuft, &lt;code&gt;ichlaufe&lt;/code&gt; heißt und dessen Konfiguration in der &lt;code&gt;config.yml&lt;/code&gt; zu finden ist. Der Runner kann im Docker über dessen &lt;a href="https://stackoverflow.com/questions/35110146/what-is-the-purpose-of-the-file-docker-sock" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Standard-Socket&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Aktionen ausführen, z.B. &lt;code&gt;ubuntu-latest&lt;/code&gt; installieren und darauf &lt;code&gt;Jekyll&lt;/code&gt; zum Laufen bringen. Die Basiskonfiguration des Runners habe ich &lt;a href="https://gitea.com/gitea/act_runner/src/branch/main/examples/docker-compose" target="_blank" rel="noopener noreferrer" class="external-link"&gt;dem offiziellen Repository&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; entnommen.&lt;/p&gt;&#10;&lt;p&gt;Gitea akzeptiert nur Runner, deren Token unter &lt;code&gt;GITEA_RUNNER_REGISTRATION_TOKEN&lt;/code&gt; registriert sind - Dies holen wir zu einem späteren Zeitpunkt nach.&lt;/p&gt;&#10;&lt;p&gt;Nun starten wir Gitea mal, sodass wir die erforderlichen Ordner zur Konfiguration erstellt bekommen.&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker compose up&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So sieht die Konsolenausgabe bei mir aus:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;[+] Running 4/2&#10; ✔ Network gitea_default Created 0.0s &#10; ✔ Network gitea_gitea Created 0.1s &#10; ✔ Container gitea Created 0.1s &#10; ✔ Container gitea-runner-1 Created 0.0s &#10;Attaching to gitea, gitea-runner-1&#10;gitea | Server listening on :: port 22.&#10;gitea | Server listening on 0.0.0.0 port 22.&#10;gitea | 2023/11/18 17:19:52 cmd/web.go:242:runWeb() [I] Starting Gitea on PID: 15&#10;gitea | 2023/11/18 17:19:52 cmd/web.go:111:showWebStartupMessage() [I] Gitea version: 1.21.0 built with GNU Make 4.4.1, go1.21.4 : bindata, timetzdata, sqlite, sqlite_unlock_notify&#10;gitea | 2023/11/18 17:19:52 cmd/web.go:112:showWebStartupMessage() [I] * RunMode: prod&#10;gitea | 2023/11/18 17:19:52 cmd/web.go:113:showWebStartupMessage() [I] * AppPath: /usr/local/bin/gitea&#10;gitea | 2023/11/18 17:19:52 cmd/web.go:114:showWebStartupMessage() [I] * WorkPath: /data/gitea&#10;gitea | 2023/11/18 17:19:52 cmd/web.go:115:showWebStartupMessage() [I] * CustomPath: /data/gitea&#10;gitea | 2023/11/18 17:19:52 cmd/web.go:116:showWebStartupMessage() [I] * ConfigFile: /data/gitea/conf/app.ini&#10;gitea | 2023/11/18 17:19:52 cmd/web.go:117:showWebStartupMessage() [I] Prepare to run web server&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="web-access"&gt;Webzugriff einrichten&lt;/h3&gt;&#10;&lt;p&gt;Nun gehe ich erneut in meinen DNS-Manager und setze die Adresse für den Webzugriff:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;TYPE NAME VALUE TTL&#10;A git.schallbert.de &amp;lt;redacted&amp;gt; 86400&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Außerdem muss die &lt;code&gt;Caddyfile&lt;/code&gt; angepasst werden, damit Anfragen aus dem Web auch richtig weitergeleitet werden können. Also zurück in den &lt;code&gt;caddy2&lt;/code&gt; Ordner und mit &lt;code&gt;nano Caddyfile&lt;/code&gt; folgenden Abschnitt hinzufügen:&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-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# caddy2/Caddyfile&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;git.schallbert.de {&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;reverse_proxy * http://gitea:3000&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nun werden Anfragen nach &lt;code&gt;git.schallbert.de&lt;/code&gt; auf den im Docker geöffneten Port &lt;code&gt;3000&lt;/code&gt; des Gitea-Containers weitergeleitet.&lt;/p&gt;&#10;&lt;h3 id="gitea-per-web-oberfläche-konfigurieren"&gt;Gitea per Web-Oberfläche konfigurieren&lt;/h3&gt;&#10;&lt;p&gt;Navigiere ich im Browser jetzt auf die entsprechende Adresse, so bekomme ich ein Login-Fenster angezeigt - genau wie in &lt;a href="https://gitea.com/user/login" target="_blank" rel="noopener noreferrer" class="external-link"&gt;dieser Demo&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Nun denke ich mir ein schönes Passwort aus, klicke auf &lt;code&gt;Register&lt;/code&gt; und bekomme hernach die Möglichkeit, Gitea per Weboberfläche zu konfigurieren.&#10;Dies tue ich aber nur rudimentär, denn ich möchte am Ende ja meine eigene Konfiguration verwenden.&lt;/p&gt;&#10;&lt;p&gt;Anschließend erstelle ich wie im Kapitel &lt;a href="https://blog.schallbert.de/projects/move-blog-to-own-server/#ssh-auf-dem-server-einrichten"&gt;SSH einrichten&lt;/a&gt; oben beschrieben ein weiteres SSH-Schlüsselpaar per &lt;code&gt;ssh-keygen&lt;/code&gt; und passendem Dateinamen &lt;code&gt;-f ~/.ssh/gitea&lt;/code&gt;. Diesen lade ich dann auf Gitea unter &lt;code&gt;Settings-&amp;gt;SSH/GPG Keys&lt;/code&gt; hoch und verifiziere ihn.&lt;/p&gt;&#10;&lt;p&gt;Nun kann ich auf meinem Rechner erneut in der &lt;code&gt;config&lt;/code&gt;-Datei des &lt;code&gt;.ssh&lt;/code&gt;-Verzeichnisses den Abschnitt zum Login auf Gitea erstellen:&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-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# .ssh/config&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Host gitea&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;User schallbert&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Hostname git.schallbert.de&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;PreferredAuthentications publickey&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Identityfile ~/.ssh/schallbert_gitea&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Jetzt schnell prüfen, ob auch alles funktioniert:&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ssh gitea&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Host &lt;span style="color:#e6db74"&gt;&amp;#39;git.schallbert.de&amp;#39;&lt;/span&gt; is known and matches the ED25519 host key.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;debug1: Found key in /home/schallbert/.ssh/known_hosts&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Perfekt.&lt;/p&gt;&#10;&lt;h3 id="gitea-konfiguration-per-appini-fertigstellen"&gt;Gitea-Konfiguration per &lt;code&gt;app.ini&lt;/code&gt; fertigstellen&lt;/h3&gt;&#10;&lt;p&gt;Da die Konfiguration aber noch nicht abgeschlossen ist, fahre ich den Container nun wieder herunter:&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker compose down&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;und navigiere nach &lt;code&gt;/gitea/gitea/conf&lt;/code&gt;. Dort öffne ich die &lt;code&gt;app.ini&lt;/code&gt; und modifiziere sie wie folgt:&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-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# conf/app.ini&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;APP_NAME&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Gitea: Git with a cup of tea&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;RUN_MODE&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;prod&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;RUN_USER&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;git&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;WORK_PATH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/gitea&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;[repository]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ROOT&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/git/repositories&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;DEFAULT_BRANCH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;main&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;[repository.local]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;LOCAL_COPY_PATH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/gitea/tmp/local-repo&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;[repository.upload]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;TEMP_PATH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/gitea/uploads&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;[server]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;APP_DATA_PATH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/gitea&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;DOMAIN&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;git.schallbert.de&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;SSH_DOMAIN&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;git.schallbert.de&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;HTTP_PORT&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;3000&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ROOT_URL&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;https://git.schallbert.de/&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;DISABLE_SSH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;SSH_PORT&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;222&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;SSH_LISTEN_PORT&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;22&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;LFS_START_SERVER&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&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:#a6e22e"&gt;LFS_JWT_SECRET&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;lt;redacted&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;OFFLINE_MODE&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;false&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;[database]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;PATH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/gitea/gitea.db&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;DB_TYPE&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;sqlite3&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;HOST&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;localhost:3306&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;NAME&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;gitea&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;USER&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;root&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;PASSWD&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:#a6e22e"&gt;LOG_SQL&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;SCHEMA&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:#a6e22e"&gt;SSL_MODE&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;disable&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;[indexer]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ISSUE_INDEXER_PATH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/gitea/indexers/issues.bleve&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;[session]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;PROVIDER_CONFIG&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/gitea/sessions&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;PROVIDER&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;file&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;[picture]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;AVATAR_UPLOAD_PATH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/gitea/avatars&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;REPOSITORY_AVATAR_UPLOAD_PATH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/gitea/repo-avatars&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;[attachment]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;PATH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/gitea/attachments&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;[log]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;MODE&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;LEVEL&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;info&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ROOT_PATH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/gitea/log&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;[security]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;INSTALL_LOCK&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&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:#a6e22e"&gt;SECRET_KEY&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:#a6e22e"&gt;REVERSE_PROXY_LIMIT&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;REVERSE_PROXY_TRUSTED_PROXIES&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;*&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;INTERNAL_TOKEN&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;lt;redacted&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;PASSWORD_HASH_ALGO&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;pbkdf2&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;[service]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;DISABLE_REGISTRATION&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&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:#a6e22e"&gt;REQUIRE_SIGNIN_VIEW&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;REGISTER_EMAIL_CONFIRM&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ENABLE_NOTIFY_MAIL&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ALLOW_ONLY_EXTERNAL_REGISTRATION&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ENABLE_CAPTCHA&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;DEFAULT_KEEP_EMAIL_PRIVATE&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;DEFAULT_ALLOW_CREATE_ORGANIZATION&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;DEFAULT_ENABLE_TIMETRACKING&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&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:#a6e22e"&gt;NO_REPLY_ADDRESS&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;noreply.localhost&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;[lfs]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;PATH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/git/lfs&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;[mailer]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ENABLED&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;false&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;[openid]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ENABLE_OPENID_SIGNIN&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&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:#a6e22e"&gt;ENABLE_OPENID_SIGNUP&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;true&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;[cron.update_checker]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ENABLED&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;false&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;[repository.pull-request]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;DEFAULT_MERGE_STYLE&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;merge&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;[repository.signing]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;DEFAULT_TRUST_MODEL&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;committer&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;[oauth2]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;JWT_SECRET&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;lt;redacted&amp;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;[actions]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ENABLED&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hier ist wichtig zu sicherzustellen, dass &lt;code&gt;ROOT_URL&lt;/code&gt; und die Portkonfiguration mit jener der &lt;code&gt;docker-compose.yml&lt;/code&gt; übereinstimmt. Die Datenbank (sqlite3) wird konfiguriert und gitea zugänglich gemacht. Außerdem deaktiviere ich aus Sicherheitsgründen die Registrierungsmöglichkeit neuer Benutzer &lt;code&gt;DISABLE_REGISTRATION = true&lt;/code&gt; sowie jene per Openid &lt;code&gt;ENABLE_OPENID_SIGNIN = false ENABLE_OPENID_SIGNUP = false&lt;/code&gt; und schalte Gitea Actions per &lt;code&gt;ENABLED = true&lt;/code&gt; ein.&lt;/p&gt;&#10;&lt;p&gt;Nun kann der Container neu gestartet 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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;+&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Running 4/4&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ✔ Network gitea_default Created 0.1s &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ✔ Network gitea_gitea Created 0.0s &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ✔ Container gitea Started 0.0s &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; x Container gitea-runner-1 Error 0.1s &#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Gut, Gitea läuft schonmal. In einem letzten Schritt kümmere ich mich nun um den Action Runner.&lt;/p&gt;&#10;&lt;h3 id="action-runner-einrichten-"&gt;Action Runner einrichten 🏃&lt;/h3&gt;&#10;&lt;p&gt;Für die ersten Schritte hilft die offizielle &lt;a href="https://docs.gitea.com/usage/actions/act-runner" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Anleitung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; weiter.&lt;/p&gt;&#10;&lt;p&gt;Ich logge mich erneut auf der Weboberfläche über &lt;code&gt;git.schallbert.de&lt;/code&gt; ein. Die Aktivierung von Actions in der &lt;code&gt;app.ini&lt;/code&gt; im vorigen Abschnitt lässt jetzt bei &lt;code&gt;Settings-&amp;gt;Actions&lt;/code&gt; ein neues Menü erscheinen.&#10;Hier klicke ich nun auf &lt;code&gt;Create new Runner&lt;/code&gt; und kopiere mir das &lt;code&gt;REGISTRATION TOKEN&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Anschließend öffne ich die &lt;code&gt;docker-compose.yml&lt;/code&gt; im Gitea-Stammverzeichnis erneut und füge wie &lt;a href="https://docs.gitea.com/usage/actions/act-runner#set-up-the-runner-using-docker-compose" target="_blank" rel="noopener noreferrer" class="external-link"&gt;hier vorgeschlagen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Folgendes ein:&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-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# gitea/docker-compose.yml&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:#f92672"&gt;runner&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;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gitea/act_runner:nightly&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;environment&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;CONFIG_FILE=/config.yml&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;GITEA_INSTANCE_URL=https://git.schallbert.de&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;GITEA_RUNNER_NAME=ichlaufe&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;GITEA_RUNNER_REGISTRATION_TOKEN= &amp;lt;redacted&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nach einem Neustart des Containers mit &lt;code&gt;docker compose down &amp;amp;&amp;amp; docker compose up -d&lt;/code&gt; erscheint nun der Runner als &amp;ldquo;gestartet&amp;rdquo; im Log:&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;✔ Network gitea_default Created 0.1s &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;✔ Network gitea_gitea Created 0.0s &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;✔ Container gitea Started 0.0s &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;✔ Container gitea-runner-1 Started 0.1s &#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hervorragend, jetzt schauen wir auf der Weboberfläche nach und siehe da:&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/movetoownserver/actionrunner.jpg" alt="Image: Gitea action runner is up and - running." /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Fehlt noch der Beweis, dass der Runner auch wirklich arbeitet.&lt;/p&gt;&#10;&lt;h3 id="funktionsprüfung-runner"&gt;Funktionsprüfung Runner&lt;/h3&gt;&#10;&lt;p&gt;Für die Prüfung des Action Runner nehme ich erneut die &lt;a href="https://blog.gitea.com/hacking-on-gitea-actions/#use-actions" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Dokumentation von Gitea&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zur Hilfe.&#10;Ich erstelle ein neues Repository auf der Gitea-Weboberfläche mit Namen &lt;code&gt;runner-test&lt;/code&gt; und füge den Inhalt der oben verlinkten Datei im Verzeichnis &lt;code&gt;/.gitea/workflows/01-test.yml&lt;/code&gt; hinzu.&lt;/p&gt;&#10;&lt;p&gt;Commit, push, und - nichts.&lt;/p&gt;&#10;&lt;p&gt;Ah, der Workflow startet nur &lt;code&gt;on: [push]&lt;/code&gt;. Er kann also gar nicht starten, wenn er gerade selbst erst auf den Server geladen wurde.&lt;/p&gt;&#10;&lt;p&gt;Gut, erstelle ich halt noch eine Datei, commit, push. Ergebnis:&#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/movetoownserver/actionsuccess.jpg" alt="Image: Gitea action runner demo passes integration" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&lt;/p&gt;&#10;&lt;p&gt;Wahnsinn! 🥳&lt;/p&gt;&#10;&lt;h2 id="zwischenfazit"&gt;Zwischenfazit&lt;/h2&gt;&#10;&lt;p&gt;Ich habe hier so getan, als wäre der Weg in einer geraden Linie verlaufen und als hätte alles reibungslos geklappt. Leider traf das so nicht zu. Es war zwar überraschend einfach, die Programme in Docker zum Laufen zu bringen, doch miteinander reden wollten sie lange nicht. Ich hatte zudem ziemliche Probleme mit der &lt;code&gt;Caddyfile&lt;/code&gt;&#10;So einfach sie auch erscheinen mag, lange wollte so gar nichts in meinem Browser erscheinen - weder Gitea noch die blog-Testseite. Ein Ping an die Seite vom Terminal des Servers aus funktionierte aber, also konnte ich den Fehler schnell auf die Konfiguration von caddy zurückführen.&lt;/p&gt;&#10;&lt;p&gt;Die Ursache: Da caddy im Docker läuft, muss die Caddyfile als Volume eingebunden werden. Im Caddyfile selber muss der Ordner vom Root des Volumes aus, also mit führendem &lt;code&gt;/&lt;/code&gt;, angegeben werden.&lt;/p&gt;&#10;&lt;p&gt;Auf der anderen Seite hat die Konfiguration von Gitea viel reibungsloser geklappt, als ich dachte. Sogar das aufwändige Freischalten des Runners, welcher auch im Docker nochmal extra geroutet werden muss, war weniger schlimm als ich dachte.&lt;/p&gt;&#10;&lt;p&gt;Mit diesem Grundgerüst habe ich jetzt einen flinken Server, welcher meinen Blog später bestimmt reibungslos hosten kann.&lt;/p&gt;&#10;&lt;p&gt;Die nächsten Schritte - das Aufsetzen des Bauprozesses für meine Website - werden &lt;a href="https://blog.schallbert.de/gitea-action-runner-native-jekyll/"&gt;in diesem Artikel hier&lt;/a&gt; und dann auch in Jekyll nativ &lt;a href="https://blog.schallbert.de/gitea-action-runner-jekyll-dockerimage/"&gt;hier&lt;/a&gt; abgehandelt.&lt;/p&gt;&#10;&lt;h2 id="update-server-architektur-im-januar-2024"&gt;Update: Server-Architektur im Januar 2024&lt;/h2&gt;&#10;&lt;p&gt;Ich habe den Server inzwischen mehrfach um Peripheriefunktionen erweitert. So habe ich mich um &lt;a href="https://blog.schallbert.de/server-protection/"&gt;Sicherheit und Backups&lt;/a&gt; ebenso gekümmert wie um &lt;a href="https://blog.schallbert.de/server-auto-upgrade/"&gt;automatische Updates&lt;/a&gt;, wobei mir die Kapselung durch Docker nicht nur einmal auf die Füße gefallen ist und einiges an Zeit gekostet hat. Auch wenn noch immer nicht alle Aspekte vollständig abgedeckt sind (Benachrichtigungen sind z.B noch ein offenes Thema), komme ich meinem Ziel, einen weitgehend selbstständig laufendes System zu schaffen, spürbar näher.&lt;/p&gt;&#10;&lt;p&gt;Am Ende möchte ich schließlich wieder dahin zurückkehren, Inhalte zu erzeugen. Und mich nicht mehr als nötig mit dem Drumherum beschäftigen.&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;CI/CD steht für &amp;ldquo;&lt;a href="https://en.wikipedia.org/wiki/Continuous_integration" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Continuous Integration&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; / &lt;a href="https://en.wikipedia.org/wiki/Continuous_deployment" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Continuous deployment&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&amp;rdquo; und bedeutet, dass das System bei Update der verwalteten Dateien automatisch die Website neu baut, testet und veröffentlicht.&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;DNS steht für &lt;a href="https://en.wikipedia.org/wiki/Domain_Name_System" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Domain Name System&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, ein verteiltes hierarchisches System zur Namensauflösung von Websites. Hintergrund ist, dass es viel schwieriger ist, sich eine IPV6-Adresse wie &lt;code&gt;2a02:ec80:300:ed1a:0:0:0:1&lt;/code&gt; zu merken statt &lt;code&gt;en.wikipedia.org&lt;/code&gt; in die Adresszeile eines Browsers einzugeben.&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></channel></rss>