<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Schallbert's Blog</title><link>https://blog.schallbert.de/en/</link><description>Recent content on Schallbert's Blog</description><generator>Hugo</generator><language>en</language><lastBuildDate>Sun, 03 May 2026</lastBuildDate><atom:link href="https://blog.schallbert.de/en/index.xml" rel="self" type="application/rss+xml"/><item><title>Hardware Security Key: add SSH</title><link>https://blog.schallbert.de/en/hardware-security-key-add-ssh/</link><pubDate>Sun, 03 May 2026</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/hardware-security-key-add-ssh/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-05-03-ssh-randomart_thumb.avif"&#10; class="post-cover"&#10; alt="Image: An SSH public key randomart image as visual fingerprint for humans to quickly see if public keys match"&#10; title="Hardware Security Key: add SSH" /&gt;&#10;&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;&#10;&lt;p&gt;In my post about &lt;a href="https://blog.schallbert.de/en/hardware-security-keys-yubikey/"&gt;hardware security keys&lt;/a&gt;, I wrote that I want to secure as many of my connections as possible without passwords provided by my new HSK. I use &lt;em&gt;SSH&lt;/em&gt; connections for various services and purposes: I access my &lt;a href="https://en.wikipedia.org/wiki/Virtual_private_server" target="_blank" rel="noopener noreferrer" class="external-link"&gt;VPS&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, use services such as GitHub, Gitea and my own &lt;a href="https://blog.schallbert.de/en/projects/move-blog-to-own-server/"&gt;Gitea instance&lt;/a&gt; and run backups on the local network via &lt;a href="https://linuxize.com/post/how-to-use-scp-command-to-securely-transfer-files/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;code&gt;scp&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, which uses the SSH protocol.&lt;/p&gt;&#10;&lt;h2 id="setting-up-ssh-access-for-hardware-security-keys"&gt;Setting up SSH access for hardware security keys&lt;/h2&gt;&#10;&lt;p&gt;HSKs such as my Yubikey can store private keys for SecureShell access. This means you no longer need to store them locally on every device (although this remains configurable; see below). Furthermore, they are better protected against unauthorised access than if stored on a computer.&lt;/p&gt;&#10;&lt;p&gt;So I&amp;rsquo;m trying to switch my GitHub SSH access to HSK and am following &lt;a href="https://www.yubico.com/blog/github-now-supports-ssh-security-keys/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Yubico&amp;rsquo;s guide to passwordless authentication for GitHub&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. The procedure for generating FIDO-compliant SSH keys is the same across different key manufacturers. I&amp;rsquo;m also linking to the &lt;a href="https://docs.nitrokey.com/nitrokeys/features/fido2/ssh" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Nitrokey guide&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; for reference.&lt;/p&gt;&#10;&lt;h2 id="what-doesnt-work"&gt;What doesn&amp;rsquo;t work&lt;/h2&gt;&#10;&lt;p&gt;However, even with Yubico&amp;rsquo;s own recommendations, I am unable to achieve the desired result on my operating system and with the standard installation of &lt;code&gt;openssh&lt;/code&gt;. The command assembled from the guide is:&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;schallbert@machine:~# ssh-keygen -t ed25519-sk -O resident -O verify-required -O application&lt;span style="color:#f92672"&gt;=&lt;/span&gt;ssh:github -O user&lt;span style="color:#f92672"&gt;=&lt;/span&gt;schallbert -C &lt;span style="color:#e6db74"&gt;&amp;#39;schallbert@github.com&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once generated, we must store the public key (file extension &lt;code&gt;.pub&lt;/code&gt;) in the web application of the respective service under the SSH Access menu item.&lt;/p&gt;&#10;&lt;h3 id="meaning-of-the-command-and-the-most-important-options"&gt;Meaning of the command and the most important options&lt;/h3&gt;&#10;&lt;p&gt;This command&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; tells &lt;a href="https://www.man7.org/linux/man-pages/man1/ssh-keygen.1.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ssh-keygen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; to generate a key of type &lt;code&gt;-t ed25519-sk&lt;/code&gt; for me. The suffix &lt;code&gt;-sk&lt;/code&gt; indicates that the key should be stored on a hardware security key - in my case, the Yubikey.&lt;/p&gt;&#10;&lt;p&gt;To understand the &lt;code&gt;-O resident&lt;/code&gt; flag, we need to know that the FIDO authenticator key, generated as with &lt;code&gt;-sk&lt;/code&gt;, consists of two parts: a &lt;code&gt;key handle&lt;/code&gt; part, which remains on the computer&amp;rsquo;s hard drive, and the private key itself, which &lt;a href="https://www.man7.org/linux/man-pages/man1/ssh-keygen.1.html#FIDO_AUTHENTICATOR" target="_blank" rel="noopener noreferrer" class="external-link"&gt;exists only on the HSM&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Only together can they correctly respond to authentication requests. The flag itself, however, instructs the program to write both parts of the key to the Yubikey. This makes it easier to use multiple devices with the same key: the &lt;code&gt;key handle&lt;/code&gt; part can be read from the Yubikey and stored on other end devices. On the other hand, however, simply losing the Yubikey is now enough to grant potential attackers SSH access.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;-f filename&lt;/code&gt; specifies the names for the key files. This is particularly useful when you need to manage multiple SSH connections or HSMs. I&amp;rsquo;ve created a naming convention to avoid confusion. It&amp;rsquo;s based on the syntax used to read keys from an HSM (&lt;code&gt;ssh-keygen -K&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-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;-f &amp;lt;keyType&amp;gt;_&amp;lt;protocol&amp;gt;:&amp;lt;service&amp;gt;_&amp;lt;user&amp;gt;_&amp;lt;device&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As a concrete example, my filename here would be as follows:&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;-f ed25519-sk_ssh:github_schallbert_yubikey-main&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;⚠️ Note when using Windows: The colon &lt;code&gt;:&lt;/code&gt; is not allowed as a character in filenames. It is replaced by an underscore &lt;code&gt;_&lt;/code&gt;, which must also be reflected in the &lt;code&gt;config&lt;/code&gt; file (&lt;code&gt;Ctrl+H&lt;/code&gt; -&amp;gt; replace &lt;code&gt;:&lt;/code&gt; with &lt;code&gt;_&lt;/code&gt;)&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;-C schallbert@github.com&lt;/code&gt; adds a comment to the key file itself, such as a contact address. This gives the key a label, which makes it easier to manage when dealing with multiple keys.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;-O application=ssh:github&lt;/code&gt; is simply for neat listing in the Authenticator and ensures that I cannot confuse one key with another. Otherwise, it would simply say &lt;code&gt;ssh:keygen&lt;/code&gt; and I would no longer be able to see which services I have stored keys for. Also helpful to discriminate keys when using the &lt;code&gt;-resident&lt;/code&gt; option in congunction with key extraction &lt;code&gt;ssh-keygen -K&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;-O user=schallbert&lt;/code&gt; has no practical use for me, as SSH is not interested in this flag and, contrary to my expectations, the Yubikey Authenticator does not display the username.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;-O no-touch-required&lt;/code&gt; prevents the hardware security key from having to be touched every time the key is used. Useful for very frequent use, though it compromises security due to the possibility of automation.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;-O verify-required&lt;/code&gt; specifies that the HSM&amp;rsquo;s PIN must be entered before the key can be used. Although intended as an additional security measure, this causes problems for me in my shell. Specifically, the PIN is requested once, but the &lt;code&gt;ssh-agent&lt;/code&gt; does not generate a prompt in my console every time. As soon as I try to connect via SSH in another session, I receive an error message: &lt;code&gt;agent refused operation&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-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# ssh -T git@github.com&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sign_and_send_pubkey: signing failed &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; ED25519-SK &lt;span style="color:#e6db74"&gt;&amp;#34;schallbert@github.com&amp;#34;&lt;/span&gt; from agent: agent refused operation&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@opnsense: Permission denied &lt;span style="color:#f92672"&gt;(&lt;/span&gt;publickey&lt;span style="color:#f92672"&gt;)&lt;/span&gt;.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="what-was-the-problem"&gt;What was the problem?&lt;/h2&gt;&#10;&lt;p&gt;After a long, frustrating troubleshooting session (What am I doing wrong? Does it work with the secondary key? Why does it work the first time but not afterwards? What if I regenerate the keys? What happens on other computers? What does &amp;lsquo;AI&amp;rsquo; say about this? - The latter certainly took an hour and left me completely stuck), I try a different command. That works straight away.&lt;/p&gt;&#10;&lt;h2 id="what-does-work-however"&gt;What does work, however&lt;/h2&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;schallbert@machine:~# ssh-keygen -t ed25519-sk -O application&lt;span style="color:#f92672"&gt;=&lt;/span&gt;ssh:github -C &lt;span style="color:#e6db74"&gt;&amp;#39;schallbert@github.com&amp;#39;&lt;/span&gt; -f ed25519-sk_ssh:github_schallbert_yubikey-main&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If I omit &lt;code&gt;verify-required&lt;/code&gt;, I can log in successfully and the SSH agent runs fine.&lt;/p&gt;&#10;&lt;h3 id="what-verify-required-requires"&gt;What &lt;code&gt;verify-required&lt;/code&gt; requires&lt;/h3&gt;&#10;&lt;p&gt;&lt;a href="https://superuser.com/questions/1804740/yubikey-ssh-authentication-fails-with-signing-failed-for-ecdsa-sk" target="_blank" rel="noopener noreferrer" class="external-link"&gt;This post&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; sheds some light on my issue. The terminal session I use to create the SSH key pair temporarily stores the PIN entry enforced by the &lt;code&gt;verify-required&lt;/code&gt; flag. That is why the login works initially. If I log in later or in a different window, as I did above to check &lt;code&gt;git pull&lt;/code&gt; in one of my repos, for example, the SSH agent does not ask for my PIN.&lt;/p&gt;&#10;&lt;p&gt;This is likely because the SSH agent itself cannot display prompts to the requesting programme. A submodule of OpenSSH provides a solution: &lt;a href="https://packages.fedoraproject.org/pkgs/openssh/openssh-askpass/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;openssh-askpass&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; . This can be passed to the agent, which then transmits the PIN prompt to the user via &lt;em&gt;askpass&lt;/em&gt;.&lt;/p&gt;&#10;&lt;h3 id="installing-ssh-askpass"&gt;Installing ssh-askpass&lt;/h3&gt;&#10;&lt;p&gt;My operating system did not have &lt;em&gt;ssh-askpass&lt;/em&gt; installed by default, so I&amp;rsquo;ll install it here. It uses the package manager &lt;code&gt;DNF&lt;/code&gt; (&amp;lsquo;Dandified YUM&amp;rsquo;) instead of &lt;em&gt;apt&lt;/em&gt; or other common managers. In my opinion, the naming convention isn&amp;rsquo;t 100% consistent: whilst I address the ssh programme in the terminal as &lt;em&gt;ssh-agent&lt;/em&gt;, I have to type &lt;em&gt;openssh-agent&lt;/em&gt; for the installation.&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;&lt;span style="color:#75715e"&gt;# verify-required needs openssh subpackage through Fedora&amp;#39;s package manager&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# sudo dnf install openssh-askpass&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Installing:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; openssh-askpass&#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;Complete!&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now I want to tell the agent that it can use &lt;em&gt;ssh-askpass&lt;/em&gt; to request the PIN. To do this, I need to know the path to the submodule.&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;schallbert@machine:~# which openssh-askpass&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/usr/bin/which: no openssh-askpass &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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Strange, I installed it above! Let&amp;rsquo;s have a look what the package manager has to say.&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;schallbert@machine:~# dnf repoquery --list openssh-askpass&#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;/usr/libexec/openssh/ssh-askpass&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ah, openssh hides in &lt;code&gt;libexec&lt;/code&gt;. Well, then I can link &lt;code&gt;ssh-agent&lt;/code&gt; to &lt;code&gt;ssh-askpass&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-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# eval &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;ssh-agent -s; SSH_ASKPASS&lt;span style="color:#f92672"&gt;=&lt;/span&gt;/usr/libexec/openssh/ssh-askpass&lt;span style="color:#66d9ef"&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;Agent pid &lt;span style="color:#ae81ff"&gt;16747&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-05-03-ssh-askpass-prompt.avif" alt="Image: Testing the ssh connection to Github with ssh -T and getting prompted for HSK device pin"&gt;&lt;/figure&gt;&#10;I check the connection to GitHub and, as expected, a dialogue box appears asking for my PIN. I then need to tap the HSK, after which I receive a success message. To ensure that this all works in a new terminal and after restarting the computer, I add a reference to the key in my configuration file.&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;&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;Host github&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; User git&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Hostname github.com&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; PreferredAuthentications publickey&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; IdentityFile ~/.ssh/ed25519-sk_ssh:github_schallbert_yubikey-main&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To be on the safe side, restart the agent and create new terminal sessions. If any login details were cached, this will reset them. When done, validate SSH connectivity.&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; schallbert@machine:~# eval &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;ssh-agent -k&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; eval &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;ssh-agent -s&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="annoying-gnome-pop-up-allow-inhibiting-shortcuts"&gt;Annoying: Gnome pop-up &amp;lsquo;Allow inhibiting shortcuts&amp;rsquo;&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-05-03-gnome-inhibit-shortcuts-prompt.avif" alt="Image: Gnome pop-up &amp;#39;Allow inhibiting shortcuts"&gt;&lt;/figure&gt;&#10;The only downside is that I now get an extra pop-up when connecting via SSH. This is triggered by the &lt;em&gt;gnome&lt;/em&gt; graphical user interface. Unfortunately, I can&amp;rsquo;t find an option to save the relevant setting permanently. As a result, it appears every time I connect with &lt;code&gt;verify-required&lt;/code&gt;. With Gnome, the problem is already &lt;a href="https://gitlab.gnome.org/GNOME/gnome-connections/-/work_items/85" target="_blank" rel="noopener noreferrer" class="external-link"&gt;known to affect other applications&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, but apparently has not yet been resolved.&lt;/p&gt;&#10;&lt;h2 id="transferring-to-windows-systems"&gt;Transferring to Windows systems&lt;/h2&gt;&#10;&lt;p&gt;I generated my keys using the &lt;code&gt;-O resident&lt;/code&gt; option. It makes sense to test the extraction of the &lt;code&gt;key-handle&lt;/code&gt; on a completely different system: &lt;em&gt;Windows&lt;/em&gt;.&lt;/p&gt;&#10;&lt;p&gt;I open the command line and type&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;schallbert@windows-machine:~# ssh-keygen -K&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Enter PIN &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; authenticator:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;You may need to touch your authenticator to authorize key download.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Provider &lt;span style="color:#e6db74"&gt;&amp;#34;internal&amp;#34;&lt;/span&gt; returned failure -1&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Unable to load resident keys: invalid format&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Invalid format? It&amp;rsquo;s often mentioned online that the keys sometimes need a &lt;a href="https://stackoverflow.com/questions/47859437/windows-10-openssh-key-invalid-format#68593742" target="_blank" rel="noopener noreferrer" class="external-link"&gt;line feed character&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; at the end to be recognized correctly. However, I cannot modify them without downloading them from HSK. So I manually transfer the public key and key handle to the computer and try out the &lt;code&gt;git pull&lt;/code&gt; command as a test. And look: it works!&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-05-03-windows-fido-key-ssh-pin-prompt.avif" alt="Image: FIDO ssh connection test on a Windows machine: OS prompts for PIN entry. It works!"&gt;&lt;/figure&gt;&#10;&lt;p&gt;So if you want to use the keys with different operating system families, the &lt;code&gt;-resident&lt;/code&gt; option doesn&amp;rsquo;t seem to be of any use. The command &lt;code&gt;ssh-keygen -K&lt;/code&gt; does not allow granularity: It will load all resident keys from the HSK at once.&lt;/p&gt;&#10;&lt;h2 id="vps-be-careful-when-replacing-keys"&gt;VPS: Be careful when replacing keys&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-05-03-hetzner-key-add-irrelevant-for-existing-servers.avif" alt="Image: Hetzner informing me that new SSH keys will not be applied to existing resources (VPS). Important to know!"&gt;&lt;/figure&gt;&#10;Another tip from personal experience: If you&amp;rsquo;ve already stored SSH keys with a service provider that hosts live-resources itself (in my case, a server as a VPS), new SSH keys are usually not passed on to that resource via the web frontend.&lt;/p&gt;&#10;&lt;p&gt;Consequently, the “old” keys are still needed, and the new ones aren&amp;rsquo;t used, even though they&amp;rsquo;re stored in the web app!&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;The new keys must be manually added to the destination.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;I follow the &lt;a href="https://community.hetzner.com/tutorials/howto-ssh-key/de#step-4---store-ssh-key-on-server" target="_blank" rel="noopener noreferrer" class="external-link"&gt;instructions from my host Hetzner&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; using the &lt;code&gt;ssh-copy-id&lt;/code&gt; command.&lt;/p&gt;&#10;&lt;h3 id="manually-store-new-keys-on-the-vps"&gt;Manually store new keys on the VPS&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;schallbert@machine:~# ssh-copy-id -i ./ed25519-sk_ssh:server_schallbert_yubikey-main.pub server &#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here, &lt;code&gt;server&lt;/code&gt; is the SSH alias for my VPS. The connection is established using the familiar “old” key, and the “new” one is saved. But something isn&amp;rsquo;t working as expected:&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;/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key&lt;span style="color:#f92672"&gt;(&lt;/span&gt;s&lt;span style="color:#f92672"&gt;)&lt;/span&gt;, to filter out any that are already installed&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/usr/bin/ssh-copy-id: WARNING: All keys were skipped because they already exist on the remote system.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#9;&#9;&lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; you think this is a mistake, you may want to use -f option&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="was-the-key-transferred-successfully"&gt;Was the key transferred successfully?&lt;/h3&gt;&#10;&lt;p&gt;So I log back into my server and check whether the new key actually appears in the &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt; file. It doesn&amp;rsquo;t. So I run the command above again, this time with the &lt;code&gt;-f (Force)&lt;/code&gt; option enabled. And sure enough, the transfer works!&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;schallbert@machine:~# ssh-copy-id -f -i ./ed25519-sk_ssh:server_schallbert_yubikey-main.pub server &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Number of key&lt;span style="color:#f92672"&gt;(&lt;/span&gt;s&lt;span style="color:#f92672"&gt;)&lt;/span&gt; added: &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;Now try logging into the machine, with: “ssh -i &amp;lt;identityFile&amp;gt; &lt;span style="color:#e6db74"&gt;&amp;#39;server&amp;#39;&lt;/span&gt;”&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;and check to make sure that only the key&lt;span style="color:#f92672"&gt;(&lt;/span&gt;s&lt;span style="color:#f92672"&gt;)&lt;/span&gt; you wanted were added.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To test it as recommended, log in with the new key and&amp;hellip; It works! Only now can the old keys be safely deleted.&lt;/p&gt;&#10;&lt;h3 id="how-to-avoid-ssh-copy-id"&gt;How to avoid ssh-copy-id&lt;/h3&gt;&#10;&lt;p&gt;If you want to transmit the public key without using additional tools, you can copy the contents of the public key file &lt;code&gt;&amp;lt;key-filename&amp;gt;.pub&lt;/code&gt; directly into a new line of &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt;. You just need an open SSH connection to your target.&lt;/p&gt;&#10;&lt;h2 id="thoughts-on-convenience-and-usability"&gt;Thoughts on convenience and usability&lt;/h2&gt;&#10;&lt;p&gt;When I look at my SSH connections, they broadly fall into two categories:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Admin access&lt;/strong&gt; (VPS, NAS, Raspberry Pi) and signatures&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Protocol access&lt;/strong&gt; (Git etc.)&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;In my view, the former require a higher level of security. Furthermore, I don&amp;rsquo;t use them as frequently as version control systems. I therefore recommend &lt;code&gt;verify-required&lt;/code&gt; for the first category and would omit this for the second. I would take the same approach with the &lt;code&gt;no-touch-required&lt;/code&gt; option: if a commit is to be uploaded every few minutes, constantly having to touch the key is rather annoying.&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;All options are displayed when you open the manual using &lt;code&gt;man ssh-keygen&lt;/code&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>Hardware Security Keys</title><link>https://blog.schallbert.de/en/hardware-security-keys-yubikey/</link><pubDate>Sat, 25 Apr 2026</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/hardware-security-keys-yubikey/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-04-25-yubikey_thumb.avif"&#10; class="post-cover"&#10; alt="Image: A brand-new hardware security key in its package on a table. The image has been altered; its colors are value-inverted."&#10; title="Hardware Security Keys" /&gt;&#10;&lt;h2 id="definitions-tpm-and-hsk"&gt;Definitions: TPM and HSK&lt;/h2&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 style="text-align: center"&gt;Acronym&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th style="text-align: center"&gt;Term&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th style="text-align: center"&gt;Brief description&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 style="text-align: center"&gt;TPM&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: center"&gt;Trusted Platform Module&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: center"&gt;Soldered securely. Protects a device&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 style="text-align: center"&gt;HSK&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: center"&gt;Hardware Security Key&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: center"&gt;Mobile. Protects accounts, certificates, private keys&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;The &lt;a href="https://de.wikipedia.org/wiki/Trusted_Platform_Module" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Trusted Platform Module&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; is a security chip that securely stores cryptographic keys and provides functions such as Secure Boot, BitLocker encryption and device identity. It is permanently soldered onto the motherboard of a computer, smartphone, etc.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-04-25-this-is-a-yubikey.avif" alt="Image: A Hardware Security Key with USB-C connector called YubiKey5 NFC."&gt;&lt;/figure&gt;&#10;A &lt;a href="https://en.wikipedia.org/wiki/Hardware_security_module" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Hardware Security Key &lt;em&gt;HSK&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, on the other hand, is usually portable and can be connected to various devices. It can not only store keys, but often also perform cryptographic operations (key generation, signing, encryption). HSKs are designed to be highly secure and tamper-proof. They can be externally certified or audited, meaning that even highly sensitive processes such as payment transactions can be carried out.&lt;/p&gt;&#10;&lt;h2 id="what-happened-so-far"&gt;What happened so far&lt;/h2&gt;&#10;&lt;p&gt;I listened to a podcast from the Bundesamt für Sicherheit in der Informationstechnik (BSI) on the topic of &lt;a href="https://www.bsi.bund.de/SharedDocs/Audio/DE/BSI/Update_verfuegbar_Folge40_2024_02_29.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&amp;lsquo;Passkeys instead of passwords&amp;rsquo;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Following the recommendations, I bought a Hardware Security Key (HSK) so that I can use &lt;a href="https://en.wikipedia.org/wiki/WebAuthn" target="_blank" rel="noopener noreferrer" class="external-link"&gt;passkeys&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; for my services in future. For a long time now, I have been unhappy with my fragmented secret management system consisting of notes, browser plugins, offline password managers such as &lt;a href="https://en.wikipedia.org/wiki/KeePass" target="_blank" rel="noopener noreferrer" class="external-link"&gt;KeePass&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://en.wikipedia.org/wiki/Secure_Shell" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;em&gt;SSH&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; credentials stored on my hard drive, backup keys and &lt;a href="https://askleo.com/how-should-i-keep-recovery-codes-secure/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;emergency codes&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; from various providers.&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;d finally like to bring some order to the situation without tying myself so tightly to a commercial provider that I become dependent on them.&lt;/p&gt;&#10;&lt;h2 id="objectives"&gt;Objectives&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;No longer having to remember passwords or login details&lt;/li&gt;&#10;&lt;li&gt;Setting up a system for managing passwords that is as simple as possible, yet secure&lt;/li&gt;&#10;&lt;li&gt;Bringing order to the jumble of accounts&lt;/li&gt;&#10;&lt;li&gt;Organising my digital legacy&lt;/li&gt;&#10;&lt;li&gt;Enabling others to do the same&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Welche Erwartung ich jetzt schon einmal nehmen kann: Ganz ohne Passwörter oder Pins geht es auch mit dem HSK nicht. Ein Yubikey ist kein Passwortmanager. Er kann zwar Zertifikate, Passkeys und Secrets für OTPs halten, gibt diese Geheimnisse dann aber nicht mehr preis. Mit seiner Hilfe kann man signieren (Authentizität beweisen), Verschlüsseln und abgeleitete Schlüssel erstellen.&lt;/p&gt;&#10;&lt;p&gt;In fact, password management can be centralised and simplified using the Yubikey. Popular password managers support passkeys meaning a master password is no longer required.&lt;/p&gt;&#10;&lt;h3 id="yubikey"&gt;Yubikey&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-04-25-yubico-authenticator.avif" alt="Image: Screenshot of Yubico Authenticator&amp;#39;s menu options, showing &amp;#39;Home&amp;#39;, &amp;#39;Accounts&amp;#39;, &amp;#39;Passkeys&amp;#39;, &amp;#39;Certificates&amp;#39;, and &amp;#39;Slots&amp;#39;"&gt;&lt;/figure&gt;&#10;As an HSK, I use two &lt;a href="https://www.yubico.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Yubikey&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; key fobs. One to carry with me at all times and one as a spare, which is kept in a safe place. There are other providers of mobile Hardware Security Keys, such as &lt;a href="https://www.nitrokey.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Nitrokey&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. I wasn&amp;rsquo;t aware of them before writing this article. That&amp;rsquo;s a shame, as their product is open-source and therefore an ideal candidate for testing on my blog 😥.&lt;/p&gt;&#10;&lt;h3 id="yubico-authenticator"&gt;Yubico Authenticator&lt;/h3&gt;&#10;&lt;p&gt;The Yubikey Authenticator GUI programme is useful for setting up the device, assigning functions to the &lt;em&gt;Slot&lt;/em&gt; key (e.g. confirmation or password entry via keyboard simulation) and if you use &lt;a href="https://www.bsi.bund.de/DE/Themen/Verbraucherinnen-und-Verbraucher/Informationen-und-Empfehlungen/Cyber-Sicherheitsempfehlungen/Accountschutz/Zwei-Faktor-Authentisierung/zwei-faktor-authentisierung_node.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;em&gt;2FA&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;-Einmalkennwörter z.B. als &lt;a href="https://en.wikipedia.org/wiki/Time-based_one-time_password" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;em&gt;TOTP&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Furthermore, my Yubikey supports &lt;a href="https://en.wikipedia.org/wiki/Public_key_infrastructure" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;em&gt;PKI&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, meaning that &lt;a href="https://letsencrypt.org/certificates/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;em&gt;SSL&lt;/em&gt; certificates&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and private keys can be stored securely.&lt;/p&gt;&#10;&lt;h2 id="concept-of-the-hsk"&gt;Concept of the HSK&lt;/h2&gt;&#10;&lt;p&gt;Part of the concept is that private keys are either generated directly on the hardware key or - without being permanently stored locally - are created on an end device but stored on the Yubikey. These remain on the key and cannot be exported. It supports common asymmetric methods such as &lt;em&gt;RSA&lt;/em&gt; and &lt;em&gt;ECDH&lt;/em&gt;. For further details, see this &lt;a href="https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/Publikationen/TechnischeRichtlinien/TR02102/BSI-TR-02102.pdf?__blob=publicationFile&amp;amp;v=15" target="_blank" rel="noopener noreferrer" class="external-link"&gt;BSI publication on cryptographic methods&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Furthermore, the key will be kept offline most of the time, i.e. without a power supply. Thus it is virtually impossible to compromise. An additional PIN as a protective measure in the event of key loss is a further security factor.&lt;/p&gt;&#10;&lt;h3 id="spare-key"&gt;Spare key&lt;/h3&gt;&#10;&lt;p&gt;Nevertheless, you should make a note of which access points you protect using which method, so that the correct accounts can be locked in the event of loss. A &amp;lsquo;&lt;a href="https://support.yubico.com/s/article/How-to-register-your-spare-key" target="_blank" rel="noopener noreferrer" class="external-link"&gt;spare key&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&amp;rsquo; should be available and set up, which is almost as time-consuming as configuring the main key.&lt;/p&gt;&#10;&lt;h3 id="nfc"&gt;NFC&lt;/h3&gt;&#10;&lt;p&gt;I&amp;rsquo;d hoped that my key&amp;rsquo;s &lt;a href="https://en.wikipedia.org/wiki/Near-field_communication" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;em&gt;NFC&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; support would make logging in even quicker and easier. However, two things bother me here: I simply cannot get my laptop to recognise the Yubikey via NFC. And on my mobile, NFC recognition works at best flakily. When I hold the key up to it, the app launches and asks me to enter the PIN. If I do so, I have to remove the key and hold it up to the sensor again (&amp;lsquo;Do not move the key!&amp;rsquo;). This doesn&amp;rsquo;t work every time by a long shot and seems rather prone to glitches overall. A pity.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-04-25-nfc-device-manager.avif" alt="Image: NFC shown in device manager of my Windows laptop: Drivers installed but not detecting my Yubikey"&gt;&lt;/figure&gt;&#10;&lt;h2 id="what-i-find-myself-missing-when-using-the-yubikey"&gt;What I find myself missing when using the Yubikey&lt;/h2&gt;&#10;&lt;p&gt;Overall, I do miss a few convenience features in my day-to-day use of the Yubikey.&#10;When I click on the desired keys or accounts, nothing happens. And it seems I can&amp;rsquo;t configure anything there either.&lt;/p&gt;&#10;&lt;p&gt;When using &lt;strong&gt;passkeys&lt;/strong&gt;, I&amp;rsquo;d like to see a shortcut. Clicking on the relevant key in the authenticator should open the corresponding app or browser window with the target website. Since I&amp;rsquo;ve already unlocked the HSK with the PIN at this point, the login process can even take place fully automatically once the key is tapped.&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;d like to see something similar when managing &lt;strong&gt;accounts&lt;/strong&gt;. Why is the module called &amp;lsquo;Accounts&amp;rsquo; if I can&amp;rsquo;t access them directly? Here, too, I have to open the relevant web apps etc. myself, have the password manager ready, and then copy the 2FA codes from the authenticator. There must be an easier way.&lt;/p&gt;&#10;&lt;h3 id="touch-password-pin-puk-management-key-passphrase"&gt;Touch, password, PIN, PUK, management key, passphrase&lt;/h3&gt;&#10;&lt;p&gt;Depending on the module, multi-factor authentication is usually required even when using the Yubikey. The simplest method is presence detection via a touch of the dongle. This ensures that a person is actively operating the HSK and cannot, for example, be logged in automatically. &lt;em&gt;Touch&lt;/em&gt; can be used across modules; for detailed configuration, see &lt;a href="https://blog.schallbert.de/en/hardware-security-key-add-ssh/"&gt;my article on SSH&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;The &lt;em&gt;Accounts&lt;/em&gt; module can optionally be secured with a password, whilst the &lt;em&gt;Passkeys&lt;/em&gt; module is secured via &lt;em&gt;PIN&lt;/em&gt;. Some providers even require a PIN to be set up as a second factor.&lt;/p&gt;&#10;&lt;p&gt;The &lt;em&gt;Certificates&lt;/em&gt; module must be protected against unauthorised access using &lt;a href="https://docs.yubico.com/yesdk/users-manual/application-piv/pin-puk-mgmt-key.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PIN, PUK and management key&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Private keys or certificates can optionally be protected with a &lt;a href="https://www.ssh.com/academy/ssh/passphrase" target="_blank" rel="noopener noreferrer" class="external-link"&gt;passphrase&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, which locks the keys themselves (and, in my view, makes no sense for the Yubikey, as the private keys cannot be read anyway).&lt;/p&gt;&#10;&lt;h2 id="feature-set-and-cryptographic-landscape"&gt;Feature set and cryptographic landscape&lt;/h2&gt;&#10;&lt;p&gt;Since the HSK promises such a significant security boost, I naturally want to make use of as many of its capabilities as possible. I&amp;rsquo;ve been playing around with the various modules for a few weeks now and have found that you really need to get your head around them and learn a lot to be able to use them all effectively. At least, once you&amp;rsquo;ve done so, you&amp;rsquo;ll have a rough idea of what the modules do and gain a good overview of internet security, encryption methods and protocols.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;“Accounts” and “Passkeys” are easy to set up for input forms (platforms, websites, shops) and apps (email clients, password managers, messengers): Highly recommended! - schallbert&lt;/p&gt;&#10;&lt;/blockquote&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;Yubikey module&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Description&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;Accounts&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Management of accounts/logins, supports TOTP&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;Passkeys&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Passwordless login procedure via &lt;a href="https://bitmi.de/it-sicherheitsblog-was-ist-eigentlich-fido2/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;FIDO/WebAuthn&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; (public key credential)&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;Certificates&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Storage and use of certificates; supports &lt;a href="https://www.openpgp.org/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;OpenPGP&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; (PGP) and &lt;a href="https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/Publikationen/TechnischeRichtlinien/TR02103/BSI-TR-02103.pdf?__blob=publicationFile&amp;amp;v=2" target="_blank" rel="noopener noreferrer" class="external-link"&gt;X.509&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; (PIV)&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;Slots&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Two programmable slots for generating static passwords or one-time passwords&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;In my opinion, things get complicated and time-consuming with public/private keys and certificates. Generation and validation procedures depend on the operating system or the programmes available on it, and instructions are sometimes insufficient or contradictory: only for nerds or professionals. - schallbert&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Let&amp;rsquo;s go through the modules.&lt;/p&gt;&#10;&lt;h3 id="two-factor-authentication-yubikey-accounts"&gt;Two-factor authentication: “Yubikey: Accounts”&lt;/h3&gt;&#10;&lt;p&gt;The Yubikey can be used as a generator for time-based one-time passwords. To do this, as with any other authenticator app, you create an account for each service and transfer a security key (secret) to the Yubikey. Once the HSK is connected, one-time codes can now be generated based on this secret.&lt;/p&gt;&#10;&lt;p&gt;As there is no way to export these secrets from the Yubikey, the process must be repeated for secondary keys. However, not every provider supports the use of multiple authenticators. So it may be sensible to keep the secret in the clipboard&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; and reuse it when configuring the secondary key(s).&lt;/p&gt;&#10;&lt;h3 id="passkeys-yubikey-passkeys"&gt;Passkeys &amp;lsquo;Yubikey: Passkeys&amp;rsquo;&lt;/h3&gt;&#10;&lt;p&gt;The Passkey module on my Yubikey works quite well. Unfortunately, many services&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; do not yet support passwordless login or only accept Passkeys as a second factor. With other services&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;, I cannot disable the password, so I see little security benefit, or I am issued with backup codes that are intended to serve as a second factor in an emergency&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;. At least, I can now bypass the password-TOTP process for some services and log in more quickly.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;lsquo;The biggest advantages of the technology for users are, on the one hand, that in future they will no longer have to create and manage passwords. On the other hand, passkeys are immune to the vast majority of known phishing attacks.&amp;rsquo; - &lt;a href="https://www.bsi.bund.de/DE/Themen/Verbraucherinnen-und-Verbraucher/Informationen-und-Empfehlungen/Cyber-Sicherheitsempfehlungen/Accountschutz/Passkeys/passkeys-anmelden-ohne-passwort_node.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;BSI website, 2025 October&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;You do not need the Authenticator app to configure passkeys. This is done entirely within the browser or the command line. Both must support the handling of passkeys.&lt;/p&gt;&#10;&lt;h3 id="keys--certificates-yubikey-certificates"&gt;Keys &amp;amp; Certificates &amp;lsquo;Yubikey: Certificates&amp;rsquo;&lt;/h3&gt;&#10;&lt;p&gt;To shed some light on this, we first need to understand the difference between a key pair and a certificate in the context of authentication. In both cases, the aim is to verify the client&amp;rsquo;s identity. This may be done with a server (e.g. via SSH), an institution (e.g. a Certificate Authority) or another client (e.g. via &lt;em&gt;PGP&lt;/em&gt;). This allows the destination to confirm that the client attempting to connect is indeed authorised to do so.&lt;/p&gt;&#10;&lt;h3 id="publicprivate-key-pair"&gt;Public/Private Key Pair&lt;/h3&gt;&#10;&lt;p&gt;A key pair alone (i.e. without a certificate) is typically used when the communication destination is controlled by the same party as the client. In such cases, the client can place the public key on the host to establish the connection. This method is used, when setting up SSH connections and for certain APIs, such as Google Search Console.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-04-25-gitea-ssh-keys.avif" alt="Image: Gitea account security settings: SSH public key stored in Gitea Webapp"&gt;&lt;/figure&gt;&#10;&lt;p&gt;The host can send a test message to the cient encrypted with the public key, which can only be converted back into plain text using the corresponding private key. If the client sends the correct plain text back to the host, this proves to the host that the connection partner is authentic.&lt;/p&gt;&#10;&lt;h3 id="certificates--piv"&gt;Certificates / PIV&lt;/h3&gt;&#10;&lt;p&gt;But what if the host is not under the client&amp;rsquo;s control? How can we then ensure that neither the client nor the server is impersonating someone else and has stored a forged public key? This is precisely where certificates are used.&lt;/p&gt;&#10;&lt;p&gt;Put simply, the certificate contains not only the public key but also additional information about the network participant and the desired connection. Now a trusted third party, uninvolved in the communication between client and server, comes into play. In the Private Key Infrastructure PKI, this is known as the Certificate Authority &lt;em&gt;CA&lt;/em&gt;.&lt;/p&gt;&#10;&lt;p&gt;The network participant now sends the public key, together with metadata about themselves, to the CA and submits a so-called &lt;em&gt;CSR&lt;/em&gt;, a Certificate Signing Request. The CA then verifies whether the participant&amp;rsquo;s channel actually belongs to them.&lt;/p&gt;&#10;&lt;p&gt;In the case of an email (keyword &lt;a href="https://www.gpg4win.org/doc/en/gpg4win-compendium_8.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PGP/GPG&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;), the CA can, for example, send an individual hash link - derived from the requestor&amp;rsquo;s public key - to the specified email address. If the link is clicked, this proves to the CA that the email address actually belongs to the client, and the CA can then issue the certificate, i.e. sign the CSR with its private key.&lt;/p&gt;&#10;&lt;p&gt;For domains (keyword SSL/TLS), the CA can check with the Domain Name Service provider whether the details match the server&amp;rsquo;s request.&lt;/p&gt;&#10;&lt;p&gt;Clients have the signatures of common Certificate Authorities stored (e.g. in the browser). If the certificate is tampered with on the return journey between server and client, its hash value changes and the client displays a certificate warning: &amp;lsquo;invalid certificate&amp;rsquo;.&#10;However, one must now trust the Certificate Authorities&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&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;The computer should be offline for this. If you want to be extra secure, carry out the pairing on a live operating system. This usually boots from a USB stick and forgets everything after shutdown.&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;E.g. Not at all: Mailbox; 2FA: Discord and Kraken, as of 2026 April. But perhaps I haven&amp;rsquo;t found the right settings?&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;E.g. Google, GitHub and Hetzner, as of 2026 April.&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;li id="fn:4"&gt;&#10;&lt;p&gt;These backup codes must also be stored securely. Ideally offline again, but definitely independently of the HSK.&amp;#160;&lt;a href="#fnref:4" 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:5"&gt;&#10;&lt;p&gt;There are alternatives to institutions such as CAs; see &lt;a href="https://de.wikipedia.org/wiki/Web_of_Trust" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Web of Trust&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. For example, identity verification can also take place peer-to-peer via uninvolved third parties.&amp;#160;&lt;a href="#fnref:5" 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>act_runner rootless: no start</title><link>https://blog.schallbert.de/en/act-runner-dind-failed-to-start-the-child/</link><pubDate>Sat, 28 Mar 2026</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/act-runner-dind-failed-to-start-the-child/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-03-28-act_runner-dind-failed-to-start-child-solved-thumb.avif"&#10; class="post-cover"&#10; alt="Image: Top page crop of OWASP&amp;#39;&amp;#39;s Docker Security Cheat Sheet [Source, downloaded May-26](https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html) [License](https://creativecommons.org/licenses/by-sa/4.0/)"&#10; title="act_runner rootless: no start" /&gt;&#10;&lt;aside class="update-box update-box--warn" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ⚠️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Gitea Retires `act_runner`&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2026-09-15T00:00:00Z"&gt;&#10; 2026-09-15&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; This article refers to an Actions implementation by Gitea, the &lt;code&gt;act_runner&lt;/code&gt;. It is derived from &lt;a href="https://github.com/nektos/act" target="_blank" rel="noopener noreferrer" class="external-link"&gt;nectos/act&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Gitea now uses &lt;a href="https://blog.gitea.com/release-of-runner-1.0.0/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;its own runner&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. The old runner should be replaced. More info: Read my post to &lt;a href="https://blog.schallbert.de/en/build-deploy-hugo-with-actions-docker-caddy/"&gt;deploy hugo with Gitea Actions, docker, and caddy&lt;/a&gt;&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;h2 id="the-error"&gt;The error&lt;/h2&gt;&#10;&lt;p&gt;My &lt;a href="https://blog.schallbert.de/en/gitea-act-runner-dind/"&gt;docker-in-docker (DinD) act_runner&lt;/a&gt; crashes shortly after starting with the following error message:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-03-28-act_runner-dind-failed-to-start-child-problem.avif" alt="Image: console window with docker logs text output: `\[rootlesskit:parent\] error: failed to start the child: fork/exec /proc/self/exe: operation not permitted`"&gt;&lt;/figure&gt;&#10;&lt;h2 id="issue-ticket-on-gitea"&gt;Issue ticket on Gitea&lt;/h2&gt;&#10;&lt;p&gt;I have created a ticket for this issue in the &lt;a href="https://gitea.com/gitea/act_runner/issues/721" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Gitea community (issue #721)&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Below, I&amp;rsquo;ll go into my own findings and summarise the discussion a little.&lt;/p&gt;&#10;&lt;h2 id="problem-with-kernel-permissions"&gt;Problem with kernel permissions?&lt;/h2&gt;&#10;&lt;p&gt;In a Docker-in-Docker configuration, the &lt;code&gt;act_runner&lt;/code&gt; must run its own &lt;a href="https://docs.docker.com/engine/security/#docker-daemon-attack-surface" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Docker daemon&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Only then can the runner create its own containers for the &lt;em&gt;actions&lt;/em&gt;. For security reasons, this is not permitted by default.&lt;/p&gt;&#10;&lt;h3 id="why-containers-are-not-allowed-to-start-other-containers"&gt;Why containers are not allowed to start other containers&lt;/h3&gt;&#10;&lt;p&gt;In the context of &lt;em&gt;act_runner&lt;/em&gt;, an action executes code that is itself part of the repository. If malicious code is introduced into the job container unnoticed, e.g. by a &amp;lsquo;collaborator&amp;rsquo;, it can, in a Docker installation without DinD, &lt;a href="https://docs.docker.com/engine/security/rootless/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;under certain circumstances&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; potentially gain direct access to the host system.&lt;/p&gt;&#10;&lt;h3 id="consequences-if-something-goes-wrong-despite-dind"&gt;Consequences if something goes wrong despite DinD&lt;/h3&gt;&#10;&lt;p&gt;When using the DinD concept, an attack can result in access to the &lt;code&gt;dockerd&lt;/code&gt; process within the &lt;em&gt;act_runner&lt;/em&gt; container - and only if the action is not properly encapsulated. Still not ideal, but acceptable: when the container is restarted, the status quo ante is restored. Access to the host system is indirect, as the container itself is equipped with kernel features.&lt;/p&gt;&#10;&lt;h3 id="simple-solution-start-the-dind-container-as-privileged"&gt;Simple solution: Start the DinD container as &lt;code&gt;privileged&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;To allow the DinD container to set up its own &lt;em&gt;actions&lt;/em&gt;, &lt;code&gt;privileged: true&lt;/code&gt; can be set in the configuration. This grants the container &lt;strong&gt;all kernel capabilities&lt;/strong&gt;. According to the (excellent) &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html#rule-3-limit-capabilities-grant-only-specific-capabilities-needed-by-a-container" target="_blank" rel="noopener noreferrer" class="external-link"&gt;OWASP Security Cheat Sheet&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, this should be avoided where possible. Should &lt;em&gt;act_runner&lt;/em&gt; itself now come under attack or reveal critical security vulnerabilities, the intruder would already have every opportunity to bypass the encapsulation from the host system.&lt;/p&gt;&#10;&lt;p&gt;If you want to take the easy route, the corresponding &lt;code&gt;docker-compose.yml&lt;/code&gt; file looks as follows.&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;# 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;# section ACT_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;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:latest-dind-rootless&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-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;privileged&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:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;And remember: Do not run containers with the &amp;ndash;privileged flag!!!&amp;rdquo; - OWASP&amp;rsquo;s Docker Security Cheat Sheet, 2026 &lt;a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;License&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h3 id="dead-end-only-grant-the-rights-that-are-absolutely-necessary"&gt;Dead end: Only grant the rights that are absolutely necessary&lt;/h3&gt;&#10;&lt;p&gt;After a quick search, I find an &lt;a href="https://www.codestudy.net/blog/can-i-run-docker-in-docker-without-using-the-privileged-flag/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;article on CodeStudy.net&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&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;, which deals with the topic of DinD. From this, I put together some changes to my &lt;code&gt;docker-compose.yml&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;My approach:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Make changes to &lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Restart the service &lt;code&gt;docker compose restart &amp;lt;service&amp;gt;&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;View logs &lt;code&gt;docker logs --tail 100 &amp;lt;container-name&amp;gt;&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;If started: Run the programme &lt;code&gt;web-app-&amp;gt;repo-&amp;gt;jobs-&amp;gt;rerun_all-jobs&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;If it fails: Repeat&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;I start with few, but very powerful, permissions. After a few iterations, I get:&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;# 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;# section act_runner DinD-rootless&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:latest-dind-rootless&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-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;privileged&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;cap_add&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;SYS_ADMIN&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;security_opt&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;no&lt;/span&gt;-&lt;span style="color:#ae81ff"&gt;new-privileges:true&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;apparmor:unconfined &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;systempaths=unconfined&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;writable-cgroups=true&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But I&amp;rsquo;m still missing permissions that lie outside kernel privileges and security zones: the Docker daemon needs to be able to access &lt;code&gt;sysfs&lt;/code&gt; and &lt;code&gt;proc&lt;/code&gt;, i.e. system file directories and processes.&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;level&lt;span style="color:#f92672"&gt;=&lt;/span&gt;warning msg&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;[rootlesskit:child ] failed to mount sysfs, falling back to read-only mount: operation not permitted&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I cannot resolve this even with &lt;code&gt;cap_add: ALL&lt;/code&gt;. Furthermore, online resources on this technical level are really scarce (and I am not a Docker specialist). A &lt;a href="https://zhsj.me/blog/view/dind-without-privileged" target="_blank" rel="noopener noreferrer" class="external-link"&gt;re-mount&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; of the file systems might still help here; but that seems too experimental and error-prone to me.&lt;/p&gt;&#10;&lt;p&gt;After several more hours of research and trial and error on my server, I have to agree with &lt;a href="https://github.com/docker-library/docker/issues/546" target="_blank" rel="noopener noreferrer" class="external-link"&gt;@tianon in a discussion on GitHub&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;: The Docker daemon requires so many permissions and capabilities that you might as well stick with &lt;code&gt;privileged: true&lt;/code&gt; and avoid having to grapple with an armada of &lt;code&gt;CAP_ADD&lt;/code&gt; and system bind mounts.&lt;/p&gt;&#10;&lt;h3 id="future-solution-virtualisation-or-daemonless"&gt;Future solution: Virtualisation or daemonless&lt;/h3&gt;&#10;&lt;p&gt;However, there may be better solutions: dedicated container runtime environments such as &lt;a href="https://github.com/nestybox/sysbox" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Sysbox&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. The box itself has no special privileges on the host system; yet, much like in a virtual machine, it appears to be able to provide applications running within it with full access and capabilities.&lt;/p&gt;&#10;&lt;p&gt;Covering everything from installation and setup to fully functional job containers would take this post too far afield. Therefore, for the time being I must refer to other &lt;a href="https://www.jaburjak.cz/posts/docker-in-docker-unprivileged/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;sources&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Switching &lt;a href="https://tiendu.github.io/2025/04/18/dind.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;from Docker to Podman&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; is also a possible solution: Podman is daemonless, rootless and offers a similar range of features to Docker. Migrating to Podman would be a project in its own right for me and does not fit within the scope of this post.&lt;/p&gt;&#10;&lt;h2 id="linux-security-modules-seccomp-apparmor-selinux"&gt;Linux Security Modules (seccomp, AppArmor, SELinux)&lt;/h2&gt;&#10;&lt;p&gt;Definition: &lt;a href="https://www.kernel.org/doc/html/latest/admin-guide/LSM/index.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;LSM&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; enable various security checks and restrictions at the kernel level. Through &amp;lsquo;Mandatory Access Control&amp;rsquo;, security extensions such as AppArmor can control kernel capabilities for individual applications and block access where necessary.&lt;/p&gt;&#10;&lt;h3 id="apparmor"&gt;AppArmor&lt;/h3&gt;&#10;&lt;p&gt;I run my server (VPS) on Ubuntu. In its more recent versions, this operating system has integrated &amp;lsquo;user namespace creation restrictions&amp;rsquo; into AppArmor, which prevents &lt;em&gt;appimages&lt;/em&gt;, &lt;em&gt;WebApps&lt;/em&gt; and &lt;em&gt;containers&lt;/em&gt; from running with elevated privileges.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Unprivileged user namespaces are a feature in the Linux kernel [&amp;hellip;]; it enables unprivileged users to gain administrator (root) permissions within a confined environment [&amp;hellip;]&amp;rdquo; - mbelair, Ubuntu Discourse, as of May-2026, &lt;a href="https://discourse.ubuntu.com/t/understanding-apparmor-user-namespace-restriction/58007" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Ubuntu Discourse Website&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;In short, this tool was developed as a &lt;a href="https://de.wikipedia.org/wiki/H%C3%A4rten_%28Computer%29" target="_blank" rel="noopener noreferrer" class="external-link"&gt;security-hardening measure&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; to reduce the attack surface on the host system when running programmes that require elevated privileges.&lt;/p&gt;&#10;&lt;h3 id="simple-solution-disable-completely"&gt;Simple solution: Disable completely&lt;/h3&gt;&#10;&lt;p&gt;The sledgehammer approach completely disables the feature for user namespaces. We tell AppArmor that third-party programmes may use kernel features or elevated privileges without restrictions, just as in older operating system versions. I found the relevant command on the &lt;a href="https://askubuntu.com/questions/1511854/how-to-permanently-disable-ubuntus-new-apparmor-user-namespace-creation-restric" target="_blank" rel="noopener noreferrer" class="external-link"&gt;AskUbuntu forum&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;It disables restrictions for this session by overwriting the kernel parameters at runtime (&lt;code&gt;-w&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-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbertTestsThis@machine:~# sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I find the command very useful for troubleshooting. If you want to find out whether the desired programme is failing to start because of AppArmor:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Enter the command above&lt;/li&gt;&#10;&lt;li&gt;Test the third-party programme, the container, etc.&lt;/li&gt;&#10;&lt;li&gt;Reboot. Or enter the command with &lt;code&gt;=1&lt;/code&gt;. This will restore the original state.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;If you wish to retain this vulnerability permanently, enter:&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;makeVulnerability@machine:~# echo &lt;span style="color:#e6db74"&gt;&amp;#39;kernel.apparmor_restrict_unprivileged_userns = 0&amp;#39;&lt;/span&gt; | sudo tee /etc/sysctl.d/20-apparmor-donotrestrict.conf&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;makeVulnerability@machine:~# sudo shutdown -r now&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This in itself does not constitute a security vulnerability. It has merely become easier, in principle, to exploit any weaknesses in the kernel and escape the container&amp;rsquo;s “sandbox”.&lt;/p&gt;&#10;&lt;h3 id="the-correct-solution-tailor-settings-for-each-application"&gt;The correct solution: Tailor settings for each application&lt;/h3&gt;&#10;&lt;p&gt;There are applications (such as my DinD version of &lt;code&gt;act_runner&lt;/code&gt;) that absolutely require elevated privileges to function properly. And only these should be granted the ability to access non-admin user namespaces.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://docs.docker.com/engine/security/apparmor/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Docker itself&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; has dedicated a section in its documentation to &lt;em&gt;AppAmor&lt;/em&gt;. To arrive at the solution for the DinD runner, a &lt;a href="https://www.spad.uk/posts/rootless-dind-noble/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;bit of transfer (thanks, @thespad)&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; is required. To recap, here is the error message from above:&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;gitea-runner | &lt;span style="color:#f92672"&gt;[&lt;/span&gt;rootlesskit:parent&lt;span style="color:#f92672"&gt;]&lt;/span&gt; error: failed to start the child: fork/exec /proc/self/exe: operation not permitted&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner | s6-svwait: fatal: some services reported permanent failure or their supervisor died&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The log file indicates that the &lt;code&gt;parent&lt;/code&gt; (Docker daemon in the container) cannot start its &lt;code&gt;child&lt;/code&gt; (runner container) because it cannot create processes (&lt;code&gt;/proc/self&lt;/code&gt;) for other participants (&lt;code&gt;fork&lt;/code&gt;). Who is the user of this daemon? &lt;code&gt;rootlesskit&lt;/code&gt;.&#10;This is exactly where our solution comes in: we need to enable the AppArmor profile for &lt;em&gt;rootlesskit&lt;/em&gt; in the act_runner&amp;rsquo;s DinD container within the &lt;code&gt;docker-compose.yml&lt;/code&gt; file.&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;# 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;# section act_runner DinD-rootless&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:latest-dind-rootless&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-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;privileged&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;security_opt&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;apparmor=rootlesskit&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The log file now looks fine. Done!&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-03-28-act_runner-dind-failed-to-start-child-solved.avif" alt="Image: act_runner rootless-DinD logfile with --privileged:true and --security_opt:apparmor=rootlesskit, showing a clean startup"&gt;&lt;/figure&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;I get the feeling that post was written by an &amp;lsquo;AI&amp;rsquo;. When it gets specific and talks about &amp;lsquo;critical volume mounts&amp;rsquo;, there are no actual system paths listed, and the article becomes so vague overall that I can&amp;rsquo;t actually reach my goal by following the advice.&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>Engrave Stainless Steel A2 / AISI304</title><link>https://blog.schallbert.de/en/engrave-aisi304-stainless-steel/</link><pubDate>Sun, 08 Mar 2026</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/engrave-aisi304-stainless-steel/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-03-08-Engrave-Stainless-thumb.avif"&#10; class="post-cover"&#10; alt="Image: A tapered engraving tool for stainless stell application"&#10; title="Engrave Stainless Steel A2 / AISI304" /&gt;&#10;&lt;h2 id="why-this-attempt"&gt;Why this attempt?&lt;/h2&gt;&#10;&lt;p&gt;I was asked if I could engrave an auxiliary anchor for a sailing yacht. This anchor is made of &lt;code&gt;10mm&lt;/code&gt; thick stainless steel sheet and, with dimensions of &lt;code&gt;389x630mm&lt;/code&gt;, fits my engraving machine.&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;ve never worked with this material before and was so curious that I accepted the job.&lt;/p&gt;&#10;&lt;h2 id="the-material"&gt;The Material&lt;/h2&gt;&#10;&lt;p&gt;The anchor is made of AISI 304 stainless steel. Other names for this alloy are &amp;lsquo;18/10&amp;rsquo;, &amp;lsquo;18/8&amp;rsquo; (referring to its chromium/nickel content), or &amp;lsquo;V2A/A2&amp;rsquo; (referring to its development from experimental melts or its production process, specifically being &amp;ldquo;&lt;strong&gt;A&lt;/strong&gt;ir-hardened&amp;rdquo; or by alloy group). It is relatively malleable and therefore frequently used in industrial applications.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-03-08-Anker-AISI304-total.avif" alt="Image: Arrow-shaped anchor with non-polished steel surface made of AISI304 stainless steel"&gt;&lt;/figure&gt;&#10;&lt;p&gt;However, this stainless steel has a few disadvantages for machining: Its thermal conductivity is quite low. Therefore, little heat is transferred to the material, which causes the milling cutter to get pretty hot. Additionally, the material is tough. The chip doesn&amp;rsquo;t detach well from the tool, easily creating &lt;a href="https://de.wikipedia.org/wiki/Aufbauschneide" target="_blank" rel="noopener noreferrer" class="external-link"&gt;built-up edges&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. This results in imperfections in the milling path. If the cutter pushes chips ahead of it, the material isn&amp;rsquo;t completely cut, and burrs tend to form. Furthermore, stainless steel is work-hardening: if the cutter rubs or dwells on the material, the pressure it exerts alters the material surface, making it harder than the tool itself.&lt;/p&gt;&#10;&lt;p&gt;All these properties combined lead to high tool wear and a tight requirements on cooling. The cutting parameters must also be kept in a narrow range.&lt;/p&gt;&#10;&lt;h2 id="the-tool"&gt;The Tool&lt;/h2&gt;&#10;&lt;p&gt;I&amp;rsquo;m choosing an engraving cutter from &lt;a href="https://shop.vhf.de/articleGroups/Gravierfraeser-fuer-Edelstahl-W_G_GFV.htm" 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; specifically designed for machining stainless steel. With a &lt;code&gt;6mm&lt;/code&gt; shank diameter, but only a 4mm cutting diameter, a short straight cutting edge, a &lt;code&gt;60°&lt;/code&gt; engraving angle, and a &lt;code&gt;0.4mm&lt;/code&gt; tip diameter, the tool appears compact and sturdy. The cutter is coated with an &lt;a href="https://www.harveytool.com/resources/tool-coatings" target="_blank" rel="noopener noreferrer" class="external-link"&gt;AlTiN&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; coating.&lt;/p&gt;&#10;&lt;p&gt;Will the thin tip withstand the milling forces and heat generated?&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-03-08-Gravierfr%c3%a4ser.avif" alt="Image: Coated carbide tapered engrave mill cutter for stainless steel"&gt;&lt;/figure&gt;&#10;&lt;h2 id="discussion-about-milling-parameters"&gt;Discussion about milling parameters&lt;/h2&gt;&#10;&lt;p&gt;In the forum &lt;a href="https://cnczone.nl/viewtopic.php?f=22&amp;amp;t=24339" target="_blank" rel="noopener noreferrer" class="external-link"&gt;cnczone.nl&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; I&amp;rsquo;m asking for recommendations on depth of cut, feed rate, and spindle speed for my solid carbide end mill.&lt;/p&gt;&#10;&lt;p&gt;My internet research&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; suggests a target cutting speed for AISI 304 of approximately &lt;code&gt;60m/min&lt;/code&gt; and a feed per tooth between &lt;code&gt;0.02mm&lt;/code&gt; and &lt;code&gt;0.05mm&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;I also called the milling cutter manufacturer and asked for recommendations for optimal use on a soft machine like mine. The manufacturer said I absolutely had to use cooling, ideally with plenty of emulsion on the material. His recommendation for the first attempt, using this milling cutter, was:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;S15000 F600 Z-0.1 (vhf-Proposal)&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;I was explained that frequent changes of direction are to be expected when engraving, and the goal should be to guide the machine as smoothly as possible with short acceleration paths. This would largely prevent chatter marks.&lt;/p&gt;&#10;&lt;p&gt;From the forum, I received values ​​between &lt;code&gt;F240&lt;/code&gt; and &lt;code&gt;F800&lt;/code&gt; at maximum spindle speed.&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;m incorporating my experience with machining steel from previous articles &lt;a href="https://blog.schallbert.de/en/milling-secc/"&gt;milling SECC&lt;/a&gt; and &lt;a href="https://blog.schallbert.de/en/milling-steel/"&gt;machining steel&lt;/a&gt;, setting a feed per tooth of &lt;code&gt;0.04mm&lt;/code&gt;. I&amp;rsquo;m adjusting the spindle speed to match the cutting speed of &lt;code&gt;60m/min&lt;/code&gt;. I like to cut quickly through the material, but with a very shallow depth of cut. This results in the following values ​​for the given cutting edge geometry:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;S30000 F1200 Z-0.1 (Schallbert)&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;With this spindle speed and feed rate, I&amp;rsquo;m exactly double the recommendation from &lt;em&gt;vhf&lt;/em&gt;. I choose a fast feed rate to minimize frictional heat at the cutter and transfer more heat to the chip. I keep the depth of cut low to reduce forces acting on the workpiece. This should result in higher engraving accuracy and better detail.&lt;/p&gt;&#10;&lt;p&gt;I consider a total engraving depth of &lt;code&gt;0.2mm&lt;/code&gt; enough.&lt;/p&gt;&#10;&lt;h3 id="machine-kinematics"&gt;Machine Kinematics&lt;/h3&gt;&#10;&lt;p&gt;As described above, spindle speed and feed rate are aligned to each other. It is important to check whether the set feed rate can actually be achieved. Depending on the machine&amp;rsquo;s acceleration capability, as well as the size and shape of the engraving, the machine may, on average, operate at a much lower feed rate due to the numerous changes in direction.&lt;/p&gt;&#10;&lt;p&gt;Therefore, the spindle speed and feed rate must be adjusted to the realistically achievable values. These can be observed with the machine in simulation mode. In my case, the machine acceleration is so high relative to the size of the engraving that I can almost always operate at target speed. Want an example calculation?&lt;/p&gt;&#10;&lt;p&gt;The machine requires the following time to accelerate from standstill to the set feed rate of &lt;code&gt;F1200&lt;/code&gt;:&lt;/p&gt;&#10;$$a_{machine} = 400\frac{mm}{s^2}, v_{feedrate} = 1200\frac{mm}{min} = 20\frac{mm}{s}$$$$t = \frac{20}{400} = 50ms$$&lt;p&gt;So, I only have very short periods where the machine is rubbing against the material more than cutting it.&lt;/p&gt;&#10;&lt;h2 id="preparing-the-engraving-process"&gt;Preparing the Engraving Process&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-03-08-AISI304-Cooling.avif" alt="Image: Material on the milling bed. Vacuum on, cooling fluid applied. Ready to mill."&gt;&lt;/figure&gt;&#10;I only work with metal very rarely. Therefore, I don&amp;rsquo;t have any ready-made cutting or cooling emulsion on hand. As a substitute, I use Ballistol spray and a little water. I rub the oil into the surface. For the &lt;code&gt;10x15cm&lt;/code&gt; image area, I add about a tablespoon of water.&lt;/p&gt;&#10;&lt;p&gt;To my surprise, the two form an emulsion on their own.&lt;/p&gt;&#10;&lt;p&gt;I plan to clamp the stock using my vacuum table and position it centrally using locator pins. I close the recesses for handle and stem insert with coated waxed paper (&lt;em&gt;Pro tip! Inexpensive, super tear-resistant, environmentally friendly, almost airtight, easy to work with&lt;/em&gt;).&lt;/p&gt;&#10;&lt;p&gt;After switching on the vacuum pump, I discover that the armature isn&amp;rsquo;t sitting completely flat on one side. Instead of &lt;code&gt;-0.7 bar&lt;/code&gt;, I&amp;rsquo;m only getting &lt;code&gt;-0.4 bar&lt;/code&gt;. This isn&amp;rsquo;t critical for the low forces involved in engraving. However, it will be difficult to maintain a consistent line width.&lt;/p&gt;&#10;&lt;p&gt;My solution is to scale down the milling design. Due to the smaller size, differences in line width will be hardly noticeable.&lt;/p&gt;&#10;&lt;h2 id="procedure"&gt;Procedure&lt;/h2&gt;&#10;&lt;p&gt;Before performing the engraving, I run the machine in simulation mode. During this process, I discover two very slow plunge steps. The CAM software likely creates these because of the plunge ramp I set, which it auto-applies to very short lines.&lt;/p&gt;&#10;&lt;p&gt;I correct this by manually setting plunge points.&lt;/p&gt;&#10;&lt;p&gt;Now I&amp;rsquo;m running the milling job. The emulsion stays in place despite the spindle fan. The noise level is significantly lower during the first depth of cut than during the second – but it never becomes unpleasant. Vibrations are minimal. Even with the more complex logo, the milling process takes barely five minutes.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-03-08-Gravur-Petrel.avif" alt="Image: Petrel logo engraved onto stainless steel anchor"&gt;&lt;/figure&gt;&#10;&lt;h2 id="result"&gt;Result&lt;/h2&gt;&#10;&lt;p&gt;I&amp;rsquo;m really happy. Based on my observations in the forums, I hadn&amp;rsquo;t expected such a good result on my hobby machine. Virtually no chatter marks, clean milling channels, and only slight burr formation at the stop and return points. However, the material remains in the milling channel and doesn&amp;rsquo;t create any perceptible edges. Even in detail, the milled finish is quite attractive.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-03-08-Gravur-Ford.avif" alt="Image: Ford logo engraved onto stainless steel anchor"&gt;&lt;/figure&gt;&#10;&lt;p&gt;The following video shows some details of the milling process. My last remaining question: Is the slightly rough noise of the milling cutter during the second depth of cut an indication of increased tool wear due to material hardening?&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;iframe&#10; src="https://makertube.net/videos/embed/7KNuywPo47HWq85FXDmyJE"&#10; title="Engraving Stainless Steel"&#10; loading="lazy"&#10; allow="autoplay; fullscreen; picture-in-picture"&#10; allowfullscreen&#10; style="width: 100%; height: auto; min-height: 315px; aspect-ratio: 16/9;"&#10; &gt;&lt;/iframe&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Engraving Stainless Steel&lt;/span&gt;&lt;a href="https://makertube.net/videos/embed/7KNuywPo47HWq85FXDmyJE" class="attr-link" target="_blank" rel="noopener noreferrer"&gt;↗&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&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;&lt;a href="https://www.machining-custom.com/blog/304-stainless-steel-cnc-turning-characteristics-and-technology.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Quelle1: VNT Hardware&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; &lt;a href="https://www.machiningdoctor.com/mds/?matId=1750" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Quelle2: Machiningdoctor&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; &lt;a href="https://www.cncoptimization.com/resources/tables/stainless-speeds-feeds/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Quelle3: CNC Optimization&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>Automate tool length measurement</title><link>https://blog.schallbert.de/en/edingcnc-macro-explained/</link><pubDate>Tue, 10 Feb 2026</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/edingcnc-macro-explained/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-02-10-tls-thumb.avif"&#10; class="post-cover"&#10; alt="Image: Drawing of Tool Length Sensor on stock material with distance spindle collet, visualizing Z0 measurement"&#10; title="Automate tool length measurement" /&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-02-10-tls.avif" alt="Image: Image: Drawing of Tool Length Sensor on stock material with distance to the spindle collet, visualizing Z0 measurement"&gt;&lt;/figure&gt;&#10;I keep receiving questions about my &lt;a href="https://blog.schallbert.de/en/macros-for-cnc/"&gt;post on CNC macros&lt;/a&gt;. In this article, I&amp;rsquo;d like to provide some answers and share details on measuring and compensating for different tool lengths.&lt;/p&gt;&#10;&lt;h2 id="question-how-does-the-macro-calculate-the-tool-length"&gt;Question: How does the macro calculate the tool length?&lt;/h2&gt;&#10;&lt;p&gt;In the section &lt;a href="https://blog.schallbert.de/en/macros-for-cnc/#function-measure-tool-length"&gt;Measuring Tool Length&lt;/a&gt;, the following code appears:&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-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;G53&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;G00&lt;/span&gt; Z&lt;span style="color:#f92672"&gt;[&lt;/span&gt;zSafety&lt;span style="color:#f92672"&gt;]&lt;/span&gt; ; &lt;span style="color:#75715e"&gt;# Go to safety height (machine coordinates)&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;2&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;G53&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;G00&lt;/span&gt; X&lt;span style="color:#f92672"&gt;[&lt;/span&gt;xPosTls&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Y&lt;span style="color:#f92672"&gt;[&lt;/span&gt;xPosTls&lt;span style="color:#f92672"&gt;]&lt;/span&gt; ; &lt;span style="color:#75715e"&gt;# Go to tool length sensor&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;3&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;G53&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;G00&lt;/span&gt; Z&lt;span style="color:#f92672"&gt;[&lt;/span&gt;zSpindleTip &lt;span style="color:#f92672"&gt;+&lt;/span&gt; toolLengthEst &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# Move Z down to 10mm above estimated tool tip&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;What happens in lines &lt;code&gt;1-3&lt;/code&gt;?&#10;If &lt;code&gt;Z=0&lt;/code&gt; is the highest position in the machine coordinate system (move with &lt;code&gt;G53&lt;/code&gt;), are &lt;code&gt;zSpindleTip&lt;/code&gt; and &lt;code&gt;toolLengthEst&lt;/code&gt; negative values?&lt;/p&gt;&#10;&lt;h3 id="answer"&gt;Answer:&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;This line moves the machine to the value stored in variable &lt;a href="https://github.com/Schallbert/macro_edingCNC_sorotec/blob/a547e36c0f47490464e1fdb56d1a4b0e706b44fe/macro.cnc#L341" target="_blank" rel="noopener noreferrer" class="external-link"&gt;#4506&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. For me, it is &lt;code&gt;0&lt;/code&gt;, i.e. maximum Z height. This value is permanently stored (see &lt;a href="https://github.com/Schallbert/macro_edingCNC_sorotec/tree/main?tab=readme-ov-file#permanent" target="_blank" rel="noopener noreferrer" class="external-link"&gt;here&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;) and must be set beforehand in the macro setup dialogue.&lt;/li&gt;&#10;&lt;li&gt;Executes a movement to the tool length sensor (WZLS) in the machine coordinate system. To do this, I have positioned my sensor at a fixed point on the machine.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;zSpindleTip&lt;/code&gt; (variable &lt;code&gt;#4509&lt;/code&gt;) refers to the height on the &lt;code&gt;Z-axis&lt;/code&gt; at which the spindle nose clears the tool length sensor. This value is negative. Why? The maximum value of the Z-axis is &lt;code&gt;zero&lt;/code&gt; when it is at the very top of the machine&amp;rsquo;s travel. If you lower the axis from this point to the tool length sensor (WZLS), you are moving it in the negative Z-direction. If you add the estimated tool length &lt;code&gt;toolLengthEst&lt;/code&gt; and a safety margin of &lt;code&gt;10mm&lt;/code&gt; (in case you made a mistake in the tool length estimation), but remain below the mechanical stop of the Z-axis, the value is still negative. However, the spindle nose now has sufficient clearance to the tool tip, because the virtual tool and safety margin have now been added.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;Pro tip: In the &amp;lsquo;Variables&amp;rsquo; tab of EdingCNC, you can view the values of a variable bank. Enter a bank, e.g. &lt;code&gt;#4500&lt;/code&gt;, and you will see the live values of all ten variables.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;To understand this properly, it is a good idea to run the CNC software in simulation mode alongside for dry testing with live variables in parallel.&#10;Concrete example:&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-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;zSafety &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# machine coordinates, Z-axis at top notch&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;zSpindleTip &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;86&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;43&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# machine coordinates, where empty spindle collet touches TLS&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;toolLengthEst &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;35&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# the length the tool is protruding from spindle collet&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;G53&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;G00&lt;/span&gt; Z&lt;span style="color:#f92672"&gt;[-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;86&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;43&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;35&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt; &lt;span style="color:#75715e"&gt;# = -41.32mm, so below max Z height&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I provide the variable names in my macro repository &lt;a href="https://github.com/Schallbert/macro_edingCNC_sorotec/tree/main?tab=readme-ov-file#permanent" target="_blank" rel="noopener noreferrer" class="external-link"&gt;on GitHub&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="question-how-do-i-calculate-the-workpiece-height-after-a-tool-measurement"&gt;Question: How do I calculate the workpiece height after a tool measurement?&lt;/h3&gt;&#10;&lt;p&gt;The following scenario: You measure the tool and then determine the workpiece height using a macro. If you now change the tool length without calling the tool change macro, the workpiece zero point is no longer correct. With a shorter tool, the zero point is &amp;rsquo;too low&amp;rsquo;; with a longer one, it is &amp;rsquo;too high&amp;rsquo;.&lt;/p&gt;&#10;&lt;p&gt;How can this problem be solved?&lt;/p&gt;&#10;&lt;h3 id="answer-several-solutions"&gt;Answer: Several solutions&lt;/h3&gt;&#10;&lt;p&gt;In my view, there are three solutions to the problem.&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Always run tool length changes through the tool change macro. You can just re-enter the same tool number there.&lt;/li&gt;&#10;&lt;li&gt;Reset the Z0 point via workpiece measurement (not recommended&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;/li&gt;&#10;&lt;li&gt;Without a macro: Use the &lt;code&gt;G92&lt;/code&gt; command to shift the Z coordinate system. Example follows.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-02-10-Enter-G92.avif" alt="Image: Coordinate shift G92 executed in the CNC software&amp;#39;s control terminal"&gt;&lt;/figure&gt;&#10;&lt;h3 id="solution-3-shift-the-coordinate-system"&gt;Solution #3: Shift the coordinate system&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-02-10-coordshift-before.avif" alt="Image: CNC coordinate window showing machine and workpiece coordinates before shifting to reflect a tool change"&gt;&lt;/figure&gt;&#10;You can shift the workpiece coordinate system using the command &lt;a href="https://www.linuxcnc.org/docs/html/gcode/g-code.html#gcode:g92" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;code&gt;G92 Coordinate System Offset&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. When executed, the number passed for the selected axis is adopted as the new value in the workpiece coordinate system.&lt;/p&gt;&#10;&lt;p&gt;In this case, we only want to shift the relative position and thus compensate for the difference in tool lengths.&lt;/p&gt;&#10;&lt;p&gt;Let&amp;rsquo;s assume that the old tool &lt;code&gt;T17&lt;/code&gt; has a free length of &lt;code&gt;32mm&lt;/code&gt; and the new tool (again &lt;code&gt;T17&lt;/code&gt;, as in this example we are simply changing tools or using a finishing tool with the same cutting data) has a free length of &lt;code&gt;24mm&lt;/code&gt;. The new value is shorter, and therefore the zero point must be moved down by &lt;code&gt;8mm&lt;/code&gt;. &amp;lsquo;Downwards&amp;rsquo; is a negative value on the Z-axis in the coordinate system.&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-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;;&lt;span style="color:#75715e"&gt;# Pseudocode&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;# (each CamelCaseWord represents a variable ID like #5003):&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;G92&lt;/span&gt; Z&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;CurrentPositionZ&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;OldToolLength&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NewToolLength&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:#75715e"&gt;# 20mm + (32mm - 24mm ) = 20mm + 8mm = 28mm &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="offset-of-workpiece-zero-vs-workpiece-coordinate-system"&gt;Offset of workpiece zero vs. workpiece coordinate system&lt;/h3&gt;&#10;&lt;p&gt;This calculation may seem confusing at first, as it shifts the coordinate system upwards. But this is precisely what is required to maintain the workpiece zero point relative to the tool tip: the tool is &lt;code&gt;8mm&lt;/code&gt; shorter. Since we do not shift the workpiece zero point downwards, the coordinate system must be shifted upwards. This compensates for the now longer distance between the tool tip and the workpiece.&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;Coordinate System&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Tool tip position Z [mm]&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Workpiece position Z [mm]&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Tool top position Z [mm]&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Workpiece position Z [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;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Before shift&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Before shift&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;After shift&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;After shift&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;Workpiece&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;20&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;28&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;Machine&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;-41.42&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;-61,42&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;-41.42&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;-69,42&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-02-10-coordshift-after.avif" alt="Image: CNC&amp;#39;s coodinate window showing machine and workpiece coordinates after shift to reflect a change of tools. The shift only affects workpiece coordinates."&gt;&lt;/figure&gt;&#10;Note that shifting the coordinate system changes the position of the tool tip in the &lt;em&gt;workpiece coordinate system&lt;/em&gt; without the Z-axis actually moving. The &lt;em&gt;machine coordinate system&lt;/em&gt; remains unaffected by this change. However, as the Z-axis now has to travel a greater distance to reach the workpiece, its position in the machine coordinate system is lower by &lt;code&gt;-8mm&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h2 id="prerequisites"&gt;Prerequisites&lt;/h2&gt;&#10;&lt;p&gt;The macros only work under certain conditions:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;The tool length has been measured beforehand, or tool changes are always performed through the macro.&lt;/li&gt;&#10;&lt;li&gt;The workpiece zero point has been determined with known tool length.&lt;/li&gt;&#10;&lt;li&gt;There hasn&amp;rsquo;t been any emergency stop or other critical error in between. This would clear the volatile memory for the relevant variables.&lt;/li&gt;&#10;&lt;/ol&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;In my view, re-measuring the workpiece height in the Z-axis after a tool change adjusts the wrong parameter. In reality, it is not the height of the workpiece that has changed, but the position of the tool tip. I would therefore always prefer to adjust the parameter that has actually changed.&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>Steel sheet (SECC) milling</title><link>https://blog.schallbert.de/en/milling-secc/</link><pubDate>Mon, 22 Dec 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/milling-secc/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-12-22-HTPC-slotted-thumb.avif"&#10; class="post-cover"&#10; alt="Image: additional slots in HTPC case (SECC) for better cooling"&#10; title="Steel sheet (SECC) milling" /&gt;&#10;&lt;h2 id="the-project"&gt;The Project&lt;/h2&gt;&#10;&lt;p&gt;This is a PC case made of &lt;code&gt;0.8mm&lt;/code&gt; thick, galvanized steel sheet &amp;ldquo;SECC&amp;rdquo;. It dates from the early 2000s and was recently fitted with new internal components.&lt;/p&gt;&#10;&lt;h2 id="the-challenge"&gt;The Challenge&lt;/h2&gt;&#10;&lt;p&gt;Due to the now much more powerful graphics card (&lt;a href="https://de.wikipedia.org/wiki/Thermal_Design_Power" target="_blank" rel="noopener noreferrer" class="external-link"&gt;TDP&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; &lt;code&gt;200W&lt;/code&gt;) and the cramped design as an &lt;a href="https://de.wikipedia.org/wiki/Home_Theater_Personal_Computer" target="_blank" rel="noopener noreferrer" class="external-link"&gt;HTPC&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, the resulting heat loss could not be dissipated as desired, resulting in temperatures around &lt;code&gt;50°C&lt;/code&gt; inside the case under load. That&amp;rsquo;s far too high for my liking.&lt;/p&gt;&#10;&lt;h3 id="the-specifications"&gt;The Specifications&lt;/h3&gt;&#10;&lt;p&gt;The graphics card follows current, common designs: A metal radiator covers the entire surface of the PCB. Its fins are oriented perpendicular to the socket, and the flat fans mounted on it (&lt;code&gt;3x80mm&lt;/code&gt;) push the air through the baffles toward the motherboard and case lid.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-12-22-graka-target-1080.avif" alt="Image: The graphics card for which cooling shall be optimized."&gt;&lt;/figure&gt;&#10;&lt;p&gt;Unfortunately, there are only a few millimeters of space between the graphics card and the case lid. Therefore, I want to add extra holes to the lid at precisely this point. The case already has &lt;code&gt;3mm&lt;/code&gt; diameter ventilation holes in a &lt;code&gt;10x19mm 45°&lt;/code&gt; grid pattern on the sides. I will now try to apply this pattern to the ventilation holes for the graphics card. This brings me to a total of 133, which I&amp;rsquo;d rather not mark, center-punch, and machine by hand for convenience.&lt;/p&gt;&#10;&lt;p&gt;So I create a drawing in CAD and then a drilling cycle program for my milling machine.&lt;/p&gt;&#10;&lt;h2 id="tool-1-3mm-drill-bit"&gt;Tool 1: 3mm Drill Bit&lt;/h2&gt;&#10;&lt;p&gt;I do have a few hardware store steel drill bits made of standard high-speed steel (HSS) lying around, but some of them are in poor condition. So I use an unused steel drill bit from my Dremel set.&lt;/p&gt;&#10;&lt;h3 id="drilling-parameters"&gt;Drilling Parameters&lt;/h3&gt;&#10;&lt;p&gt;It has a light gray, metallic color and looks so similar to uncoated solid carbide tools that I (mistakenly) assume it is carbide. Therefore, I selected the following parameters:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;3mm solid carbide drill bit for steel, 2 flutes: S17000 F470 Z-0.2, retraction 0.5mm&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;As usual, I work completely dry, meaning I don&amp;rsquo;t use any coolant.&lt;/p&gt;&#10;&lt;h3 id="drilling-preparation"&gt;Drilling Preparation&lt;/h3&gt;&#10;&lt;p&gt;To prepare the workpiece, I apply masking tape to the section of the housing where the holes will later be drilled. Using the maximum drilling depth, I only penetrate the material minimally. Since the masking tape remains intact this way, I don&amp;rsquo;t lose vacuum through exposed holes in the vacuum table.&lt;/p&gt;&#10;&lt;p&gt;I place the housing cover, outer side down, on the machine bed and cover any unused holes in my vacuum table with a rubber mat. Then I position the gantry close to the holes so that the spindle doesn&amp;rsquo;t collide with the housing wall during the machining path. I also remove the move to park position &lt;code&gt;G28&lt;/code&gt; from the milling file, as the housing cover is now in the way. After the Z-axis is raised from the last hole, the program ends with &lt;code&gt;M30&lt;/code&gt; (stop and rewind).&lt;/p&gt;&#10;&lt;h3 id="the-result"&gt;The Result&lt;/h3&gt;&#10;&lt;p&gt;To cut to the chase: The drill bit didn&amp;rsquo;t last. I noticed something was wrong after just a few seconds from the sound of the machine: It was suddenly much quieter than before.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-12-22-hss-meltdown.avif" alt="Image: The HSS drill only lasts to the fourth hole of 133, then its tip is gone."&gt;&lt;/figure&gt;&#10;&lt;p&gt;I managed a measly 3 out of 133 holes this way. After that, the tip of the drill bit was stuck in one of the partially completed holes and seemed to be welded to it.&lt;/p&gt;&#10;&lt;h3 id="problem-analysis"&gt;Problem Analysis&lt;/h3&gt;&#10;&lt;p&gt;Apparently, the drill bit wasn&amp;rsquo;t made of solid carbide at all, and therefore I was using completely unsuitable parameters. I posted the symptoms in two forums (&lt;a href="https://hobbyline.info/forum/index.php?thread/687-2-schneider-stahlblech-wie-anwenden/&amp;amp;postID=12837#post12837" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Hobbyline&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and &lt;a href="https://www.cnczone.nl/viewtopic.php?t=23051" target="_blank" rel="noopener noreferrer" class="external-link"&gt;cnczone&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;) and received the information that I should have been using &lt;code&gt;S1000 F30&lt;/code&gt; (!) if I wanted to drill a few more holes.&lt;/p&gt;&#10;&lt;h3 id="the-solution"&gt;The Solution&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-12-22-slots.avif" alt="Image: Slot milling solves the problem as I can use a carbide endmill now"&gt;&lt;/figure&gt;&#10;So I&amp;rsquo;m switching to carbide end mills. I only have one in my collection that was made for steel. However, it has a larger diameter, so I have to adjust my design.&lt;/p&gt;&#10;&lt;p&gt;Now I&amp;rsquo;m creating slots in CAD, which are &lt;code&gt;6mm&lt;/code&gt; wide and &lt;code&gt;22mm&lt;/code&gt; long, positioned at a &lt;code&gt;45°&lt;/code&gt; angle above the graphics card radiator.&lt;/p&gt;&#10;&lt;h2 id="tool-2-6mm-solid-carbide-end-mill"&gt;Tool 2: 6mm Solid Carbide End Mill&lt;/h2&gt;&#10;&lt;p&gt;I&amp;rsquo;m applying my already &lt;a href="https://blog.schallbert.de/en/milling-steel/"&gt;proven milling technique&lt;/a&gt; for SECC and refining it further: High spindle speed, high feed rate, shallow depth of cut.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;6mm solid carbide end mill, AlTiN-coated, 2 flutes, 0.2mm 45° chamfer: S10600 F880 Z-0.18&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;iframe&#10; src="https://makertube.net/videos/embed/hoqrsaBrW2nrjszyJTPEpC"&#10; title="Cutting SECC steel of my PC case"&#10; loading="lazy"&#10; allow="autoplay; fullscreen; picture-in-picture"&#10; allowfullscreen&#10; style="width: 100%; height: auto; min-height: 315px; aspect-ratio: 16/9;"&#10; &gt;&lt;/iframe&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Cutting SECC steel of my PC case&lt;/span&gt;&lt;a href="https://makertube.net/videos/embed/hoqrsaBrW2nrjszyJTPEpC" class="attr-link" target="_blank" rel="noopener noreferrer"&gt;↗&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h3 id="cut-quality"&gt;Cut Quality&lt;/h3&gt;&#10;&lt;p&gt;The cut edges are burr-free, high-gloss, and razor-sharp. Therefore, a chamfer needs to be applied. I do this by hand with a scraper and utility knife, which I quickly regret: I can&amp;rsquo;t get it as clean and even as I&amp;rsquo;d like.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-12-22-slots-complete.avif" alt="Image: SECC surface quality after milling &amp;amp; de-burr"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Not so bad: I get a neatly finished case with significantly improved cooling practically for free (apart from the learning curve). In the image above, the graphics card&amp;rsquo;s radiator is clearly visible through the new slots in the case.&lt;/p&gt;&#10;</description></item><item><title>Jekyll-dockerimage: Bundler and Gemfile</title><link>https://blog.schallbert.de/en/bundler-ci-gemfile-issue/</link><pubDate>Fri, 14 Nov 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/bundler-ci-gemfile-issue/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-11-15-bundler-errors.avif"&#10; class="post-cover"&#10; alt="Image: bundler logo with error overlay"&#10; title="Jekyll-dockerimage: Bundler and Gemfile" /&gt;&#10;&lt;h2 id="what-is-bundler"&gt;What is &lt;em&gt;bundler&lt;/em&gt;?&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://bundler.io/guides/faq.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;em&gt;Bundler&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; is a tool that can be used to manage and version dependencies between modules and libraries for &lt;a href="https://www.ruby-lang.org/en/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;em&gt;Ruby&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; language. I use &lt;em&gt;Bundler&lt;/em&gt; commands regularly, e.g. to build my blog and put it live on the server.&lt;/p&gt;&#10;&lt;p&gt;If I want to build locally, I type&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;bundle exec jekyll serve --incremental --future&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;into the console. This command tells &lt;em&gt;Bundler&lt;/em&gt; to run the &lt;em&gt;jekyll&lt;/em&gt; application in server mode and provides it with parameters that prevent &lt;em&gt;jekyll&lt;/em&gt; from completely rebuilding every time a file is changed and still create pages that have not yet been published.&lt;/p&gt;&#10;&lt;h2 id="bundler-in-the-ci-pipeline"&gt;&lt;em&gt;bundler&lt;/em&gt; in the CI pipeline&lt;/h2&gt;&#10;&lt;p&gt;I also use &lt;em&gt;Bundler&lt;/em&gt; as part of a Jekyll Docker image to publish my site, as I have often linked to, e.g. when moving to &lt;a href="https://blog.schallbert.de/en/projects/move-blog-to-own-server/"&gt;self-hosted&lt;/a&gt;. Since switching to a new version of the blog software lately, I have been seeing puzzling errors in my CI pipeline. Locally however, the system builds flawlessly. I have documented how to address and fix such errors here.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;There was an error while trying to write to /path/to/Gemfile.lock&amp;rdquo; - CI console output&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h3 id="no-write-permissions"&gt;No write permissions&lt;/h3&gt;&#10;&lt;p&gt;Like the &lt;em&gt;Jekyll&lt;/em&gt; user, &lt;em&gt;Bundler&lt;/em&gt; itself can only read files on my CI and cannot write them. I can fix this for the &lt;em&gt;Jekyll&lt;/em&gt; output with &lt;code&gt;chown&lt;/code&gt;, but I don&amp;rsquo;t want to allow &lt;em&gt;bundler&lt;/em&gt; to do this: I require &lt;code&gt;Gemfile.lock&lt;/code&gt; to remain identical between my local build environment and the CI so that I can fix errors in advance.&lt;/p&gt;&#10;&lt;h3 id="specification-file-for-bundler"&gt;Specification file for &lt;em&gt;bundler&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;The &lt;code&gt;Gemfile.lock&lt;/code&gt; file contains all dependencies of the application used, including version numbers and sources. If only the source code of the application plus this file is provided, &lt;em&gt;bundler&lt;/em&gt; can pull a specific version of all dependencies during build time, allowing to work with similar requirements regardless of machine and version.&lt;/p&gt;&#10;&lt;h3 id="troubleshooting"&gt;Troubleshooting&lt;/h3&gt;&#10;&lt;p&gt;A first attempt to fix the issue by aligning all versions between local and CI failed.&#10;The second attempt, to give the Jekyll user write permissions to the &lt;code&gt;Gemfile.lock&lt;/code&gt;, also failed.&#10;The third attempt led me to the Bundler website, where I took a closer look at the parameters, keyword “frozen”.&lt;/p&gt;&#10;&lt;p&gt;The fact is that you can prohibit &lt;em&gt;bundler&lt;/em&gt; from rewriting the &lt;code&gt;Gemfile.lock&lt;/code&gt;. To do this, use the command&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;bundle config set frozen true&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;According to its &lt;a href="https://bundler.io/v2.7/man/bundle-config.1.html#LIST-OF-AVAILABLE-KEYS" target="_blank" rel="noopener noreferrer" class="external-link"&gt;documentation&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, processing is aborted as soon as the file is about to be rewritten.&lt;/p&gt;&#10;&lt;p&gt;Although I had added this command to my &lt;code&gt;yaml&lt;/code&gt; file in the CI, the build failed again. The trigger was the same library &lt;a href="https://nokogiri.org/#" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;em&gt;nokogiri&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; as before, but the error message was now much more helpful:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-11-14-gemfile-lock-incompatible.avif" alt="Image: jekyll build error due to build platform incompatibility between local and remote."&gt;&lt;/figure&gt;&#10;&lt;h2 id="optimize-gemfilelock-for-different-environments"&gt;Optimize &lt;code&gt;Gemfile.lock&lt;/code&gt; for different environments&lt;/h2&gt;&#10;&lt;p&gt;So I follow the suggestion and execute the desired command on my local machine:&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;bundle lock --add-platform x86_64_musl &lt;span style="color:#75715e"&gt;# my CI runner&amp;#39;s environment&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note: &lt;a href="https://musl.libc.org/about.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;musl&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; is a &lt;em&gt;libc&lt;/em&gt;-implementation for Linux.&lt;/p&gt;&#10;&lt;p&gt;My lockfile now has the following entry:&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;nokogiri (1.18.10-x86_64-linux-musl)&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;With this error fixed, I get better portability of my web page creation setup as a side effect.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-11-14-jekyll-yml-freeze-bundler.avif" alt="Image: successful CI run with updated dependencies"&gt;&lt;/figure&gt;&#10;</description></item><item><title>fail2ban: Configuration error</title><link>https://blog.schallbert.de/en/fail2ban-error-configuration-bad-value/</link><pubDate>Thu, 23 Oct 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/fail2ban-error-configuration-bad-value/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-09-23-docker-chain-thumb.avif"&#10; class="post-cover"&#10; alt="Image: iptables view for docker-user chain"&#10; title="fail2ban: Configuration error" /&gt;&#10;&lt;p&gt;Every few weeks, I usually check my server:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Is everything OK with Docker? &lt;code&gt;docker ps&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Are the backups being created correctly? &lt;code&gt;borgmatic repo-info&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Are my defenses working? &lt;code&gt;iptables -n -L&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Are my update automation daemons running? &lt;code&gt;htop -F /server-config&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;This time, however, &lt;code&gt;iptables&lt;/code&gt; showed a blank page on the &lt;code&gt;DOCKER-USER&lt;/code&gt; chain. This is unusual and indicates that something is wrong with &lt;em&gt;fail2ban&lt;/em&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-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# iptables -n -L&#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;Chain FORWARD &lt;span style="color:#f92672"&gt;(&lt;/span&gt;policy DROP&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;target prot opt source destination &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;DOCKER-USER all -- 0.0.0.0/0 0.0.0.0/0 &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;DOCKER-FORWARD all -- 0.0.0.0/0 0.0.0.0/0 &#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;Chain DOCKER-USER &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; references&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;target port opt source destination &#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The output reads something like&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;The &lt;code&gt;FORWARD&lt;/code&gt; chain forwards, as required by &lt;em&gt;Docker&lt;/em&gt;, to the container-relevant &lt;code&gt;DOCKER-USER&lt;/code&gt; chain. However, no applications are listed under this chain that can reject or prohibit packets from being forwarded (&lt;code&gt;REJECT/DROP&lt;/code&gt;).&amp;rdquo;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Two entries for &lt;code&gt;caddy-server&lt;/code&gt; and &lt;code&gt;caddy-ratelimit&lt;/code&gt; should appear here.&lt;/p&gt;&#10;&lt;h2 id="fail2ban-error-failed-during-configuration"&gt;&amp;ldquo;fail2ban ERROR Failed during configuration&amp;rdquo;&lt;/h2&gt;&#10;&lt;p&gt;So I first check Docker:&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;docker logs -t fail2ban &lt;span style="color:#75715e"&gt;# Print logs for the container named fail2ban with a timestamp&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Instead of being rewarded with &lt;code&gt;&amp;lt;timestamp&amp;gt; Server ready&lt;/code&gt; after scrolling through as usual, I see a never-ending flood of log messages. They repeat themselves and show that &lt;em&gt;fail2ban&lt;/em&gt; is exiting with &lt;code&gt;Exit with code 255&lt;/code&gt;. I get a better overview of the log if I only print the last 100 lines:&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;docker logs -t --tail &lt;span style="color:#ae81ff"&gt;100&lt;/span&gt; fail2ban &lt;span style="color:#75715e"&gt;# return latest 100 lines of log with timestamp&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="the-error-message"&gt;The error message&lt;/h3&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;&amp;lt;timestamp, ID&amp;gt; ERROR Failed during configuration: Bad value substitution:&#10;option &amp;#39;action&amp;#39; in section &amp;#39;caddy-ratelimit&amp;#39; contains an interpolation key &amp;#39;banaction&amp;#39; which is not a valid option name.&#10;Raw value: &amp;#39;%(action_)s&amp;#39;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This message is interesting because I had problems with &lt;code&gt;jail.local&lt;/code&gt; at exactly this point when I was creating the article &lt;a href="https://blog.schallbert.de/en/fail2ban-with-caddy/#check-jail"&gt;Fail2ban with Caddy&lt;/a&gt;. At the time, I had to remove &lt;code&gt;action = iptables-multiport&lt;/code&gt; from the configuration because the triggering IP addresses had landed on the wrong &lt;code&gt;CHAIN&lt;/code&gt; (INPUT).&lt;/p&gt;&#10;&lt;p&gt;Now, the default action on the &lt;code&gt;DOCKER-USER&lt;/code&gt; chain seems to no longer work. So, I&amp;rsquo;m searching a few sources for a possible solution. However, I can&amp;rsquo;t find any references to updates that would cause major changes in the &lt;code&gt;action.d&lt;/code&gt; folder either in the update history of &lt;em&gt;fail2ban&lt;/em&gt; or in the &lt;a href="https://docs.linuxserver.io/images/docker-fail2ban/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;linuxserver.io&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; distribution I&amp;rsquo;m using.&lt;/p&gt;&#10;&lt;h3 id="solution-via-banaction--iptablestypemultiport"&gt;Solution via &lt;code&gt;banaction = iptables[type=multiport]&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;So I tried the web search and found a comment from a &lt;a href="https://github.com/fail2ban/fail2ban/issues/3894" target="_blank" rel="noopener noreferrer" class="external-link"&gt;fail2ban maintainer&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;[&amp;hellip;] With action = iptables[type=multiport, protocol=tcp, chain=DOCKER-USER] you overwrote the default action [&amp;hellip;] [Solution:] simply set the chain variable only in the jail [&amp;hellip;]: &lt;code&gt;port = http, https banaction = iptables[type=multiport] chain = DOCKER-USER&lt;/code&gt;&amp;rdquo; - &lt;a href="https://github.com/sebres" target="_blank" rel="noopener noreferrer" class="external-link"&gt;sebres&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, Dec-2024&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;The basis for this statement can be found in the &lt;a href="https://docs.docker.com/engine/network/packet-filtering-firewalls/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Docker Docs&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Side note: In all places, it is recommended to use the more modern &lt;em&gt;nftables&lt;/em&gt; instead of &lt;em&gt;iptables&lt;/em&gt; for future applications. &lt;a href="https://linux-audit.com/networking/nftables/differences-between-iptables-and-nftables-explained/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Here&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; is an article on the topic.&lt;/p&gt;&#10;&lt;p&gt;This allows me to completely delete the &lt;code&gt;action&lt;/code&gt; field that was previously present in my &lt;code&gt;jail.local&lt;/code&gt; and use &lt;code&gt;banaction&lt;/code&gt; instead. It now looks like this:&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;# jail.local update Oct-2025&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;# Server/Client errors: 401 403 404 500&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-status]&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;enabled = true&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;chain = DOCKER-USER&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;port = http,https&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;filter = caddy-status&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;logpath = &amp;lt;path/to/logfiles&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:#ae81ff"&gt;findtime = 10min&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;maxretry = 5&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;bantime = 1d&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;banaction = iptables[type=multiport]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With this configuration, &lt;em&gt;fail2ban&lt;/em&gt; boots normally and displays &lt;code&gt;Server ready&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="verifying-the-solution-using-iptables"&gt;Verifying the solution using &lt;em&gt;iptables&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;As described above, I create entries in the test log files that should trigger the actions of &lt;code&gt;caddy-status&lt;/code&gt; and &lt;code&gt;caddy-ratelimit&lt;/code&gt;. As expected, I get the following in the &amp;ldquo;blocklist&amp;rdquo;:&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;schallbert@machine:~# iptables -n -L&#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;Chain DOCKER-USER &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; references&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;target prot opt source destination &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;f2b-caddy-ratelimit tcp -- 0.0.0.0/0 0.0.0.0/0 multiport dports 80,443&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;f2b-caddy-status tcp -- 0.0.0.0/0 0.0.0.0/0 multiport dports 80,443&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Milling Albizia/Albasia</title><link>https://blog.schallbert.de/en/albasia-on-cnc/</link><pubDate>Mon, 06 Oct 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/albasia-on-cnc/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-10-06-albasia-on-cnc-thumb.avif"&#10; class="post-cover"&#10; alt="Image: Macro shot of Albasia plywood, layer view"&#10; title="Milling Albizia/Albasia" /&gt;&#10;&lt;h2 id="what-is-albasia"&gt;What is Albasia?&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-10-06-albasia-samplecolors.avif" alt="Image: Albasia sandwich material samples with colored melamine surface"&gt;&lt;/figure&gt;&#10;Albasia wood (&lt;a href="https://en.wikipedia.org/wiki/Falcataria_falcata" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Falcataria falcata&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;), also known as Albizia, is a very light softwood. It is found primarily in &lt;a href="https://www.plywoodworking.com/exploring-albasia-wood-a-sustainable-solution-for-the-timber-industry.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Southeast Asia&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and grows rapidly, which improves its &lt;a href="https://www.juergensen.de/news/albasia-sperrholz-unsere-gruene-loesung-fuer-die-industrie/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;sustainability rating&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Its color varies between whitish-yellow and pastel brown-pink, but is significantly lighter than beech or oak.&lt;/p&gt;&#10;&lt;p&gt;In Europe, it is usually sold as plywood. This mitigates disadvantages such as the strong tendency to shrink and warp.&lt;/p&gt;&#10;&lt;p&gt;The density of Albasia is about 15% lower than that of the already very light poplar wood. It forms long fibers as it grows, which impairs its machinability. Because it also provides good insulation due to its numerous air pockets, it is often used in vehicle interiors (campers, boats, etc.). To compensate for its low strength, Albasia multiplex is often offered as a sandwich panel rather than raw.&lt;/p&gt;&#10;&lt;h3 id="sandwich-technology-achieves-good-material-properties"&gt;Sandwich technology achieves good material properties&lt;/h3&gt;&#10;&lt;p&gt;My test material has a &lt;code&gt;0.8mm&lt;/code&gt; thick layer of high-pressure laminate (HPL) applied on both sides. The surface is available in colored melamine decors and can be ordered with a hardened finish, which makes it matte and scratch-resistant.&lt;/p&gt;&#10;&lt;h3 id="are-albasia-composite-panels-suitable-for-outdoor-use"&gt;Are Albasia composite panels suitable for outdoor use?&lt;/h3&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Our panels are not intended for outdoor use.&amp;rdquo; - EpicPLY, Retailer&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Even though the material was developed for interior construction, I&amp;rsquo;m currently testing the weather resistance of the panels with an experiment. I&amp;rsquo;m screwing the composite panels together at right angles, butting them together so that one side of the coating and the other side of the wood edge are directly exposed to the elements. I&amp;rsquo;ve sealed one exposed edge with hard oil and left the other untreated. Now I&amp;rsquo;m hanging the structure outside on the balcony for a year and will then see how the material behaves – an update will follow in a separate post!&lt;/p&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&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-0"&#10; aria-label="Image: test material and tools: plywood sample, srews, drills"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2025-10-06-albasia-outdoor-test-setup.avif"&#10; alt="Image: test material and tools: plywood sample, srews, drills"&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-1"&#10; aria-label="Image: Albasia weather test mounted outside"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2025-10-06-albasia-outdoor-test-mount.avif"&#10; alt="Image: Albasia weather test mounted outside"&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-2"&#10; aria-label="Image: Comparison of oiled and raw plywood surface on day0 of test. Both look fresh and new."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2025-10-06-albasia-outdoor-test-surface.avif"&#10; alt="Image: Comparison of oiled and raw plywood surface on day0 of test. Both look fresh and new."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;/div&gt;&lt;div class="hugo-gallery__caption"&gt;&#10; Setup for weathering and screw mounting tests with Albizia plywood.&#10; &lt;/div&gt;&lt;/div&gt;&#10;&#10; &lt;div class="hugo-gallery__full" aria-live="polite"&gt;&lt;figure&#10; id="gallery-full-0"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2025-10-06-albasia-outdoor-test-setup.avif"&#10; alt="Image: test material and tools: plywood sample, srews, drills"&gt;&lt;figcaption&gt;Vorbereitung für den Schraubentest&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-1"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2025-10-06-albasia-outdoor-test-mount.avif"&#10; alt="Image: Albasia weather test mounted outside"&gt;&lt;figcaption&gt;Der zusammengesetzte Testaufbau ist zur Bewitterung angebracht.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-2"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2025-10-06-albasia-outdoor-test-surface.avif"&#10; alt="Image: Comparison of oiled and raw plywood surface on day0 of test. Both look fresh and new."&gt;&lt;figcaption&gt;Am Tag 0: Beide Oberflächen sehen noch frisch aus.&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&#10;&lt;/section&gt;&#10;&#10;&lt;p&gt;My expectation: unlike birch plywood and similar to poplar wood, Albasia will not be suitable for outdoor use. I assume that the fibers in the edge area will absorb water and cause the material to swell. This would quickly lead to its decay. Furthermore, I consider the risk of insect infestation to be high because the fibers are really soft and can be easily peeled off. Therefore, I would only use it indoors.&lt;/p&gt;&#10;&lt;h3 id="screw-connections"&gt;Screw connections&lt;/h3&gt;&#10;&lt;p&gt;Despite its light core, the Albasia composite panel is easy to screw together and, with a &amp;ldquo;screw pull-out strength of 120kg&amp;rdquo; for a &lt;code&gt;4mm&lt;/code&gt; wood screw, is in a &amp;ldquo;very stable range&amp;rdquo;, according to my dealer.&lt;/p&gt;&#10;&lt;p&gt;As a layperson I can&amp;rsquo;t make much sense of such figures, so I conduct a small experiment. I take a &lt;code&gt;4x35mm&lt;/code&gt; wood screw and look at the specified tightening torques. For my manufacturer, the following applies:&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;&lt;strong&gt;Parameter&lt;/strong&gt;&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;&lt;strong&gt;Value&lt;/strong&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;Pull-out parameter \( f_{ax,k} \)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;14 N/mm²&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;Torsional strength \( f_{tor,k} \)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;3 Nm&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;Screw-in torque \( R_{tor,mean} \)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;\( \frac{f_{tor,k}}{R_{tor,mean}} \) ≥ 1.5&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;Pre-drilling in softwood&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2.5 mm&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;Pre-drilling in hardwood&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;3 mm&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;&lt;a href="https://www.spax.com/de-de/p/universalschraube-teilgewinde-senkkopf-t-star-plus-4cut-wirox.html?variant=0191010400353" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Source&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;Now I&amp;rsquo;m checking if I can butt-join two boards with the specified tightening torque. For this, I&amp;rsquo;m using a torque screwdriver. But I don&amp;rsquo;t even get to the point where the mechanism engages: the screw starts to pull itself through the surface layer before that happens. I estimate the maximum torque before the screw &amp;ldquo;disappears&amp;rdquo; into the material to be only &lt;code&gt;0.8Nm&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Now I&amp;rsquo;m checking if I can butt-join two boards with the specified tightening torque.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2025-10-06-albasia-fastener-test.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Video: Trying to tighten a screw to the correct torque in wood.&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;This method certainly allows for stable screw connections, but I would always proceed carefully when tightening, use a generous number of screws, and additionally choose dowel or form-fitting connection methods.&lt;/p&gt;&#10;&lt;h2 id="albasia-on-the-cnc-router"&gt;Albasia on the CNC router&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--left"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-10-06-6mm-schlicht.avif" alt="Image: fine-cutting Albasia with a 6mm carbide endmill"&gt;&lt;/figure&gt;&#10;Albasia can be processed with a CNC router like any other softwood. Problems typical of softwoods, such as tear-outs or fiber clusters, are to be expected here as well. In addition, the hard coating makes machining even more difficult due to different parameter selection requirements.&lt;/p&gt;&#10;&lt;h3 id="which-router-bits-are-suitable-for-machining"&gt;Which router bits are suitable for machining?&lt;/h3&gt;&#10;&lt;p&gt;The material is challenging for the router bit. I had already had bad experiences with milling HPL in the article &lt;a href="https://blog.schallbert.de/en/cnc-router-overload/"&gt;about overloading my router motor&lt;/a&gt;. However, the material at that time had a much higher proportion of hard paper fibers compared to Albasia composite (chips were light brown and interspersed with many &amp;ldquo;dots&amp;rdquo;), which blunted the router bit in record time.&lt;/p&gt;&#10;&lt;p&gt;I use solid carbide router bits with chip breaker and minimal helix (15°) to process the long wood fibers. Otherwise, they quickly clog the extraction system and accumulate around the router bit, which can pose a fire hazard.&lt;/p&gt;&#10;&lt;p&gt;For edge processing, I&amp;rsquo;ve had good experiences with compressive router bits like the quarter round bit. If you only plunge slightly into the material, a V-cut is also suitable. With a greater plunge depth, half-round &amp;ldquo;ball-nose&amp;rdquo; router bits achieve a nice milling finish.&lt;/p&gt;&#10;&lt;p&gt;The image shows a comparison. On the right, the raw material has a green-gray coating, cut on a panel saw. On the left, in purple, is the workpiece I CNC-milled. The cutting pattern is clearly different and the surface machined with the milling cutter is much cleaner and without any tears.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-10-06-albasia-mill-vs-saw.avif" alt="Image: Albasia / Albizia plywood sandwiched between HPL coatings. Saw cut reveals to be much more coarse with pull-outs versus the milled surface."&gt;&lt;/figure&gt;&#10;&lt;p&gt;If you&amp;rsquo;re using chipbreaking cutters in the full groove, it&amp;rsquo;s worth adding a finishing pass with a straight-toothed cutter and high feed rates with only a small infeed of &lt;code&gt;0.2mm&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;All cutters must have the following in common:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;a cutting edge as sharp as possible (no coating!)&lt;/li&gt;&#10;&lt;li&gt;a high cutting speed (don&amp;rsquo;t use diameters that are too small)&lt;/li&gt;&#10;&lt;li&gt;a relatively high feed rate.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I was able to achieve a high surface quality with single-flute cutters. The large chip channel ensures good removal, but chipping can sometimes be problematic when single-passing with full engagement. The cutting load is quite high.&lt;/p&gt;&#10;&lt;h3 id="which-parameters"&gt;Which parameters?&lt;/h3&gt;&#10;&lt;p&gt;Albasia is so soft that it offers little resistance to the milling machine&amp;rsquo;s axis drives. The cutting speed should be set high; online sources recommend around &lt;code&gt;500m/min&lt;/code&gt; as a guideline. I&amp;rsquo;ve tried feed rates up to &lt;code&gt;6000mm/min&lt;/code&gt;. As I only machined small workpieces, even higher feed rates would hardly offer any time advantage due to the constant changes in direction. An additional finishing pass is mandatory due to chipbreaker marks.&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;ve noted a good compromise between milling time and surface quality for various cutters below. However, I only use the engraving cutters in the HPL layer, which is why I reduce the feed and speed slightly.&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;Tool (s = number of cutting edges)&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Feed rate F [mm/min]&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Speed S [rpm]&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Z feed [mm]&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;XY feed [mm]&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;Solid carbide 2s chipbreaking 15° 6mm&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;4200&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;12&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2.7&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&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;Solid carbide 2s straight 6mm&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;3700&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;26500&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;21&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0.2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&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;Solid carbide V-cut 3s 90° 10mm&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;21000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0.27&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0.1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&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;Solid carbide radius cutter 2s 6mm&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;21000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0.27&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0.1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&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;Solid carbide quarter round 4s r2 6mm&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;3600&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;2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="finishing-pass"&gt;Finishing pass&lt;/h3&gt;&#10;&lt;p&gt;The following video shows the finishing process after roughing a &lt;code&gt;145x145mm&lt;/code&gt; workpiece. Due to the small lateral infeed, hardly any fiber strings form, and the low feed rate ensures a very high surface quality despite the slight twist of the cutter.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2025-10-06-albasia-schlicht.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;CNC finishing of Albasia composite material&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="engraving"&gt;Engraving&lt;/h2&gt;&#10;&lt;p&gt;With coated Albasia panels, engraving is ideally done in the top layer. If the material is not penetrating the substrate, problems with tearing and loose fibers can be avoided. Since the top layer is not very thick, the choice quickly falls on V-shaped cutters with an obtuse angle or a flattened tip.&lt;/p&gt;&#10;&lt;p&gt;Alternatively, pockets of 3D engravings can be cleared with end mills, if the CAM program offers such an option. This creates completely smooth &amp;ldquo;floors&amp;rdquo;. Alternatively, radius cutters can be used due to their gently rising geometry in the lower area. If the CAM program does not support 3D engraving with radius cutters, the cutter geometry can be approximated as described in the article &lt;a href="https://blog.schallbert.de/en/halftone-image-on-dibond/#simulation-radius-cutter"&gt;Halftone Images on Dibond&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;I have prepared two examples in the image below: The CR code in the upper half was created with a &lt;code&gt;6mm&lt;/code&gt; radius cutter at a &lt;code&gt;0.2mm&lt;/code&gt; plunge depth. The resulting dots and lines feel soft and pleasant thanks to the smooth transitions. The lettering in the lower half of the image was milled with an engraving cutter and a depth of up to &lt;code&gt;7mm&lt;/code&gt;. While this beautifully showcases the natural wood tone, small letter heights like this (max. &lt;code&gt;30mm&lt;/code&gt;) tend to cause breakouts in letters with &amp;ldquo;islands&amp;rdquo; (a, e, b, o, etc.).&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-10-06-albasia-engrave-compare.avif" alt="Image: Albasia sandwich material engraved. Upper part using a radius endmill not penetrating the HPL cover, yields a dark and very clean finish but lacks contrast. Lower part with a classic 60° V-cut with a brighter wooden finish but with artifacts"&gt;&lt;/figure&gt;&#10;&lt;h2 id="working-with-color"&gt;Working with Color&lt;/h2&gt;&#10;&lt;p&gt;The contrast of the engravings is OK, but the legibility is far from perfect. Therefore, it makes sense to ink the engravings. I experimented with a few colors; you can see some impressions in the image below.&lt;/p&gt;&#10;&lt;h3 id="which-varnish"&gt;Which varnish?&lt;/h3&gt;&#10;&lt;p&gt;The board comes with a protective film to protect the colored coating. This makes it super easy to apply spray paint if I leave the protective film on the workpiece for engraving. I used standard water-based acrylic varnish (top right), spray paint with metallic particles (below), and a solvent-based permanent marker (top left).&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-10-25-engrave-compare.avif" alt="Image: Impressions of colored engravings of the name *Schallbert* and the QR code to this website."&gt;&lt;/figure&gt;&#10;&lt;p&gt;The varnishing process was easy. The abrasion resistance of the surfaces engraved in HPL is high with color applied. If you engrave through to the wood, the fibers will stand up slightly due to the varnishing, which is very time-consuming to correct later and not beneficial to the overall impression.&lt;/p&gt;&#10;&lt;h3 id="please-note-protective-film"&gt;Please note: Protective film&lt;/h3&gt;&#10;&lt;p&gt;The varnishing process should be started immediately after engraving, if possible, and not days or even weeks later. Otherwise, the protective film tends to peel off or bubble at the edges of the engraving, and the color is drawn into areas where it doesn&amp;rsquo;t belong by capillary action. Unfortunately, this visibly ruined the gold-blue version of my test series.&lt;/p&gt;&#10;&lt;h2 id="conclusion-what-is-albasia-for"&gt;Conclusion: What is Albasia for?&lt;/h2&gt;&#10;&lt;p&gt;All in all, I would consider Albasia to be one of the more docile materials for CNC machining, accepting a wide range of feed parameters while maintaining a high surface quality. A challenge is its tendency to form long fibers. Due to the gluing in the multiplex composite, poor quality of individual inner layers can become apparent late in the processing, which then leads to scrap and time wasted.&lt;/p&gt;&#10;&lt;p&gt;It&amp;rsquo;s also suitable as an engraving material, but the coating alone would be sufficient for this, and the small engraving depths place strict limits. Therefore, the engraving can at best be a nice addition to the actual use of the panels.&lt;/p&gt;&#10;&lt;p&gt;The coating makes the material quite suitable for furniture construction. If you work with concealed edges, I can even imagine it being used in the kitchen. Because it has high flexural stability and the coating&amp;rsquo;s impressive hardness, it&amp;rsquo;s a surface suitable for everyday use. The panel material is feather-light, making it ideal for mobile applications in visible areas.&lt;/p&gt;&#10;&lt;p&gt;However, it&amp;rsquo;s nowhere near as robust as birch plywood. I wouldn&amp;rsquo;t trust the material to withstand stressed joints; for bolted connections, I would maximize surface area and always use furniture connectors (i.e., avoid wood screws).&lt;/p&gt;&#10;</description></item><item><title>passtdas - a helper for your CNC</title><link>https://blog.schallbert.de/en/passtdas/</link><pubDate>Mon, 08 Sep 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/passtdas/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-09-08-passtdas-thumb.avif"&#10; class="post-cover"&#10; alt="Image: Visualizazion of how passtdas analyzes and determines workpiece edges"&#10; title="passtdas - a helper for your CNC" /&gt;&#10;&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;&#10;&lt;p&gt;A friend of mine wrote to me. He uses a CNC gantry milling machine as a hobby, just like me, and often produces unique pieces. However, the resulting leftovers are sometimes too valuable to throw away. To maximize material utilization, he tries to use these offcuts as blanks for future projects.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Do you know how I can find out on the machine whether my milled part fits on the blank?&amp;rdquo; - Anonymous&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;This happens: According to the dimensions, the workpiece just fits on the blank. If you set the zero point even slightly incorrectly, the milling cutter will move beyond the workpiece boundaries, and scrap it is.&lt;/p&gt;&#10;&lt;h2 id="the-solution"&gt;The Solution&lt;/h2&gt;&#10;&lt;p&gt;It would be great if the machine followed the contour of the workpiece before starting the milling process, wouldn&amp;rsquo;t it? This way, you could easily find out whether the blank is actually completely within the workpiece boundaries.&lt;/p&gt;&#10;&lt;p&gt;Since I can&amp;rsquo;t find a quick solution online right away, I&amp;rsquo;m thinking about writing my own software to implement this. So I&amp;rsquo;m getting to work.&lt;/p&gt;&#10;&lt;p&gt;TODO: ADD VIDEO LINK&lt;/p&gt;&#10;&lt;h2 id="the-software"&gt;The Software&lt;/h2&gt;&#10;&lt;p&gt;For a quick prototype, I choose the high-level language &lt;a href="https://www.python.org/downloads/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;python&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Why? I have a fairly good command of &lt;em&gt;python&lt;/em&gt;, libraries for the necessary geometric calculations are available, and the written program can be easily compiled into an all-in-one file.&lt;/p&gt;&#10;&lt;p&gt;The program is designed to read machine instructions (G-code) and analyze the milling paths. The positions at which the maximum and minimum X and Y values ​​are approached in the milling program are to be saved and output as a motion command in a path file.&lt;/p&gt;&#10;&lt;p&gt;If this program is loaded onto the machine, an &amp;ldquo;extreme value&amp;rdquo; determined in this way is approached and the Z-axis is lowered from the safety height to the measuring height. The program is then paused so that the operator can make adjustments to the workpiece position. Only by entering another start command will the next extreme value be reached, and so on.&lt;/p&gt;&#10;&lt;p&gt;This program will be called &lt;em&gt;passtdas&lt;/em&gt;.&lt;/p&gt;&#10;&lt;h3 id="program-type"&gt;Program type&lt;/h3&gt;&#10;&lt;p&gt;I&amp;rsquo;m choosing a console program with few parameters:&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;Shortcut&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Parameter name&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Description&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;code&gt;-f&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;--file&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;The source file to be analyzed. Required.&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;&lt;code&gt;-s&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;--zsafety&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Safety height the machine will do flyovers with. Optional.&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;&lt;code&gt;-p&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;--zprobe&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Probe height the machine will pinpoint to. Optional.&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;The two height parameters are given default values, so they don&amp;rsquo;t always have to be specified.&lt;/p&gt;&#10;&lt;h3 id="input"&gt;Input&lt;/h3&gt;&#10;&lt;p&gt;&lt;em&gt;passtdas&lt;/em&gt; reads files and searches them line by line for motion commands like &lt;code&gt;G00, G01, G02, G03&lt;/code&gt;. While the analysis is simple for linear movements like &lt;code&gt;G00, G01&lt;/code&gt; – linear movements can be expressed using point-to-point connections whose extreme values ​​cannot lie between the points – it can be quite challenging for circular segments.&lt;/p&gt;&#10;&lt;p&gt;The height information on the Z-axis is also read and saved. This allows the machining technician to quickly check whether the maximum milling depth matches the desired one.&lt;/p&gt;&#10;&lt;p&gt;If the G-code file contains no instructions or incorrectly formulated instructions, &lt;em&gt;passtdas&lt;/em&gt; displays corresponding error messages and aborts the analysis.&lt;/p&gt;&#10;&lt;h3 id="definition-of-circular-segments"&gt;Definition of Circular Segments&lt;/h3&gt;&#10;&lt;p&gt;Background: Circular segments can be defined in two ways in G-code.&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;&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;G00 X5.660 Y-0.000 Z10.000&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G02 X-5.660 Y0.000 R5.660 F1600&#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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Command:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Move a circular arc clockwise from point &lt;code&gt;X5.66 Y0&lt;/code&gt; with radius &lt;code&gt;5.66mm&lt;/code&gt; to point &lt;code&gt;X-5.66 Y0&lt;/code&gt; with feed rate &lt;code&gt;1600mm/min&lt;/code&gt; at height &lt;code&gt;10mm&lt;/code&gt;.&amp;rdquo;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Drawing these commands creates a hanging semicircle. Circles can be defined using three points. However, only two points are given. The third point must be calculated from the specified arc radius using the start and end points.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-09-08-arcr.avif" alt="Image: drawing an arc using given radius, start- and endpoint"&gt;&lt;/figure&gt;&#10;&lt;p&gt;&lt;em&gt;Task: Find the extreme points of this arc in the XY plane.&lt;/em&gt;&lt;/p&gt;&#10;&lt;p&gt;But an arc can also be defined like this:&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;&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;G00 X-2.5696 Y5.0431&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G03 X-4.6258 Y3.2616 Z-0.5000 I2.5696 J-5.0431&#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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Command:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Traverse a circular arc counterclockwise from point &lt;code&gt;X-2.56 Y5.04&lt;/code&gt; with center coordinates &lt;code&gt;I2.56 J-5.04&lt;/code&gt; to point &lt;code&gt;X-4.62 Y3.26&lt;/code&gt;.&amp;rdquo;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Drawing these commands creates a short circular segment in the upper left quadrant. Although three points (start and end points, as well as the center of the circle) are given, it can sometimes be difficult to determine whether an extreme value is being reached somewhere along the arc.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-09-08-arcij.avif" alt="Image: drawing an arc using center point, start- and endpoint"&gt;&lt;/figure&gt;&#10;&lt;p&gt;&lt;em&gt;Task: Find the extreme points of this circle in the XY range.&lt;/em&gt;&lt;/p&gt;&#10;&lt;h3 id="calculating-the-extreme-points-of-circle-segments"&gt;Calculating the extreme points of circle segments&lt;/h3&gt;&#10;&lt;p&gt;A circle segment can have up to four additional extreme points, depending on its range: X+, Y+, X-, Y-. For a full circle, these are always the coordinate intersection points as seen from the radius.&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_extremes_from_arc&lt;/span&gt;(arc, coordinates):&#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; x_plus_radius &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [center_x &lt;span style="color:#f92672"&gt;+&lt;/span&gt; radius, center_y, xyz[&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; y_plus_radius &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [center_x, center_y &lt;span style="color:#f92672"&gt;+&lt;/span&gt; radius, xyz[&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; x_minus_radius &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [center_x &lt;span style="color:#f92672"&gt;-&lt;/span&gt; radius, center_y, xyz[&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; y_minus_radius &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [center_x, center_y &lt;span style="color:#f92672"&gt;-&lt;/span&gt; radius, xyz[&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; extremevalue_order &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [x_plus_radius, y_plus_radius, x_minus_radius, y_minus_radius]&#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; (arc[&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;] &lt;span style="color:#f92672"&gt;-&lt;/span&gt; arc[&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;]) &lt;span style="color:#f92672"&gt;&amp;lt;&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:#75715e"&gt;# crossing the 0° line (x-axis), handle overflow with nested if below&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; arc[&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;360&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; result &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;for&lt;/span&gt; crossing_angle &lt;span style="color:#f92672"&gt;in&lt;/span&gt; range(&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;721&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;90&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; crossing_angle &lt;span style="color:#f92672"&gt;in&lt;/span&gt; range(int(arc[&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;]), int(arc[&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; i &lt;span style="color:#f92672"&gt;=&lt;/span&gt; int(crossing_angle &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;90&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; i &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; i &lt;span style="color:#f92672"&gt;-=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; result&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append(extremevalue_order[i])&#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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If the circle center and radius are known, the swept angle can be determined from the start and end points. This makes it clear whether the circle&amp;rsquo;s start and/or end points themselves represent extreme values, or whether additional extreme values ​​arise from sweeping over a coordinate origin. It is precisely what the above code example does.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-09-08-arcext.avif" alt="Image: Possible coordinate extreme values ​​of an arc segment"&gt;&lt;/figure&gt;&#10;&lt;h3 id="data-table"&gt;Data table&lt;/h3&gt;&#10;&lt;p&gt;All possible extreme values ​​(two for a line, up to four for a circle segment) are now collected in a data table. Once each G-code line has been evaluated and the list is thus complete, the maximum and minimum values ​​for each axis are determined from this list.&lt;/p&gt;&#10;&lt;p&gt;This ultimately creates four XYZ coordinates, which describe the maximum extent of the workpiece for each spatial direction.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-09-08-ext.avif" alt="Image: Visual example of an extreme value dataset for a simple geometry"&gt;&lt;/figure&gt;&#10;&lt;p&gt;At the same time, the program searches all commands to the Z-axis and remembers the maximum plunge depth.&lt;/p&gt;&#10;&lt;h3 id="output"&gt;Output&lt;/h3&gt;&#10;&lt;p&gt;The command-line program has no visual output. Therefore, I&amp;rsquo;ll limit myself to creating a simulation program for the milling machine in which the workpiece extension is approached and paused at each point:&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;G90&#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;# Go to coordinate zero&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;MSG &lt;span style="color:#e6db74"&gt;&amp;#34;Zmin of this job: -1.0&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G00 Z40&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G00 X0 Y0&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G01 Z15 F1200&#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;MSG &lt;span style="color:#e6db74"&gt;&amp;#34;PathPreview: Hit START to go to Ymin: [&amp;#39;0.0&amp;#39;, &amp;#39;-5.66&amp;#39;]&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;M00&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G00 Z40&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G00 X0.0 Y-5.66&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G01 Z15 F1200&#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;MSG &lt;span style="color:#e6db74"&gt;&amp;#34;PathPreview: Hit START to go to Xmin: [&amp;#39;-5.66&amp;#39;, &amp;#39;0.0&amp;#39;]&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;M00&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G00 Z40&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G00 X-5.66 Y0.0&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G01 Z15 F1200&#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;MSG &lt;span style="color:#e6db74"&gt;&amp;#34;PathPreview: Hit START to go to Ymax: [&amp;#39;0.0&amp;#39;, &amp;#39;5.66&amp;#39;]&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;M00&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G00 Z40&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G00 X0.0 Y5.66&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G01 Z15 F1200&#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;MSG &lt;span style="color:#e6db74"&gt;&amp;#34;PathPreview: Hit START to go to Xmax: [&amp;#39;5.66&amp;#39;, &amp;#39;0.0&amp;#39;]&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;M00&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G00 Z40&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G00 X5.66 Y0.0&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;G01 Z15 F1200&#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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The above example traverses the extreme values ​​of a circle with &lt;code&gt;r=5.66mm&lt;/code&gt;. It always moves to a safety height of &lt;code&gt;Z40&lt;/code&gt; to cover the points and, upon arrival, slowly moves the Z axis at &lt;code&gt;F1200&lt;/code&gt; to the target height of &lt;code&gt;Z15&lt;/code&gt;. It is important that the zero point in &lt;code&gt;Z&lt;/code&gt; has been correctly measured beforehand – otherwise, a collision with the workpiece may occur.&lt;/p&gt;&#10;&lt;h2 id="testing-and-verification"&gt;Testing and Verification&lt;/h2&gt;&#10;&lt;p&gt;To see if the program works as expected, I created three test files:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Unit tests: Tests individual functions in the program, e.g., whether line and circle segments are correctly recognized&lt;/li&gt;&#10;&lt;li&gt;Integration tests: Tests whether functions function correctly in conjunction, e.g., the correct calculation of extreme values ​​based on input values&lt;/li&gt;&#10;&lt;li&gt;End-to-end tests: These tests check the entire function chain, e.g., whether incorrectly formatted input files are recognized&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-09-08-unittest.avif" alt="Image: Test result summary for passtdas"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Finally, I create several test files with the program and move to the extreme values ​​generated from them with my CNC.&lt;/p&gt;&#10;&lt;h2 id="open-source-software"&gt;Open-source software&lt;/h2&gt;&#10;&lt;p&gt;The source code for &lt;em&gt;passtdas&lt;/em&gt; is available for free on &lt;a href="https://github.com/Schallbert/passtdas" 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;, but must be interpreted using Python.&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;ll build a user-friendly and easy-to-use program once enough people have expressed a desire for it.&lt;/p&gt;&#10;</description></item><item><title>Rootless *act_runner* for Gitea</title><link>https://blog.schallbert.de/en/gitea-act-runner-dind/</link><pubDate>Sun, 10 Aug 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/gitea-act-runner-dind/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-08-10-gitea-act-runner-dind-rootless-thumb.avif"&#10; class="post-cover"&#10; alt="Image: Symbolized Docker containers next to text &amp;#39;&amp;#39;rootless act_runner DinD&amp;#39;&amp;#39;"&#10; title="Rootless *act_runner* for Gitea" /&gt;&#10;&lt;aside class="update-box update-box--warn" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ⚠️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Gitea Retires `act_runner`&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2026-09-15T00:00:00Z"&gt;&#10; 2026-09-15&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; This article refers to an Actions implementation by Gitea, the &lt;code&gt;act_runner&lt;/code&gt;. It is derived from &lt;a href="https://github.com/nektos/act" target="_blank" rel="noopener noreferrer" class="external-link"&gt;nectos/act&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Gitea now uses &lt;a href="https://blog.gitea.com/release-of-runner-1.0.0/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;its own runner&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. The old runner should be replaced. More info: Read my post to &lt;a href="https://blog.schallbert.de/en/build-deploy-hugo-with-actions-docker-caddy/"&gt;deploy hugo with Gitea Actions, docker, and caddy&lt;/a&gt;&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;p&gt;In this article, I&amp;rsquo;ll show how to change the &lt;em&gt;act_runner&lt;/em&gt; of my &lt;em&gt;Gitea&lt;/em&gt; instance from &lt;code&gt;gitea/act_runner&lt;/code&gt; to &lt;code&gt;gitea/act_runner:latest-dind-rootless&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Why all this effort? Because &lt;code&gt;act_runner&lt;/code&gt; runs in &lt;em&gt;Docker&lt;/em&gt; and requires access to the daemon via &lt;code&gt;/var/run/docker.sock&lt;/code&gt; as a so-called volume (meaning a disk) or bind mount to function. The owner of this socket is &lt;code&gt;root&lt;/code&gt;, which gives the container virtually full access to the host system.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;RULE #1 - Do not expose the Docker daemon socket (even to the containers)&amp;rdquo; - OWASP / Docker Security Cheat Sheet&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;This is highly risky, because it&amp;rsquo;s inherent in the runner&amp;rsquo;s design to execute job code generated by third parties. It&amp;rsquo;s an open gateway. In conjunction with direct access to the host system, this could result in a total failure or hostile takeover of my infrastructure in the event of a successful attack.&lt;/p&gt;&#10;&lt;h2 id="danger-from-dockersock"&gt;Danger from &lt;code&gt;docker.sock&lt;/code&gt;&lt;/h2&gt;&#10;&lt;p&gt;In this guide, I follow the &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html#docker-security-cheat-sheet" target="_blank" rel="noopener noreferrer" class="external-link"&gt;OWASP&amp;rsquo;s recommendation on Docker security&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and apply its rule(s) in practice. OWASP stands for &amp;ldquo;Open Web Application Security Project.&amp;rdquo; It is an organization dedicated to improving the security of web applications and supporting users like me with free articles, documentation, and technology.&lt;/p&gt;&#10;&lt;p&gt;The measures described under &lt;strong&gt;Rule 1&lt;/strong&gt; are:&lt;/p&gt;&#10;&lt;h3 id="leave-dockers-tcp-socket-disabled"&gt;Leave Docker&amp;rsquo;s &lt;em&gt;tcp&lt;/em&gt; socket disabled&lt;/h3&gt;&#10;&lt;p&gt;If access to the &lt;em&gt;Docker Daemon&lt;/em&gt; is enabled via &lt;em&gt;tcp&lt;/em&gt;, it can be connected to via an unsecured connection and without authentication - unless further precautions have been taken. The daemon is then accessible to virtually any internet user and thus vulnerable.&lt;/p&gt;&#10;&lt;p&gt;So how do I ensure that the &lt;em&gt;tcp&lt;/em&gt; socket is disabled?&#10;The instructions for this can be found in the &lt;a href="https://docs.docker.com/engine/daemon/remote-access/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Docker Docs&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and must be applied in reverse. There, too, there is an explicit warning against opening the &lt;em&gt;tcp&lt;/em&gt; socket unprotected.&lt;/p&gt;&#10;&lt;p&gt;After completing all the necessary steps, I run &lt;a href="https://en.wikipedia.org/wiki/Netstat" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;code&gt;netstat&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; once. It lists open sockets, network interfaces, and routing tables:&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;&lt;span style="color:#75715e"&gt;# check if Docker Daemon&amp;#39;s &amp;#34;dockerd&amp;#34; tcp socket is exposed&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# netstat -lntp | grep dockerd&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# test is a pass if this command does not return anything.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I don&amp;rsquo;t find any entry with &lt;code&gt;dockerd&lt;/code&gt;. My server isn&amp;rsquo;t vulnerable at this point.&lt;/p&gt;&#10;&lt;h3 id="do-not-include-the-docker-socket-varrundockersock-in-other-containers"&gt;Do not include the Docker socket &lt;code&gt;/var/run/docker.sock&lt;/code&gt; in other containers&lt;/h3&gt;&#10;&lt;p&gt;This is where things get a bit more complicated: &lt;em&gt;act_runner&lt;/em&gt; needs the socket to create, manage, and ultimately dispose of job containers. Without access to the &lt;em&gt;Docker Daemon&lt;/em&gt; via the socket, the build pipeline simply won&amp;rsquo;t work – unless you want to forgo &lt;em&gt;Docker&lt;/em&gt; entirely and run both runner and the build jobs directly on the host machine. This comes with many disadvantages: loss of encapsulation, lack of portability, poor scalability, reduced security&amp;hellip;&lt;/p&gt;&#10;&lt;p&gt;But there&amp;rsquo;s also a solution for the &lt;em&gt;Docker&lt;/em&gt; option: &lt;a href="https://gitea.com/gitea/act_runner/src/branch/main/examples/docker-compose#running-act_runner-using-docker-in-docker-dind" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Setting up act_runner Docker-in-Docker&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. With this setup, &lt;em&gt;act_runner&lt;/em&gt; receives its own &lt;em&gt;Docker Daemon&lt;/em&gt;, but with limited permissions and without access to the host system. This then takes over the lifecycle of job containers so that they run completely independently of the host system.&lt;/p&gt;&#10;&lt;h2 id="docker-in-docker"&gt;Docker in Docker&lt;/h2&gt;&#10;&lt;p&gt;The following diagram illustrates the difference:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-08-10-runner-dind-setup.avif" alt="Image: act_runner with standard configuration versus DinD-rootless. The latter has an isolated Docker Daemon running within the act_runner container."&gt;&lt;/figure&gt;&#10;&lt;h3 id="starting-act_runner-rootless"&gt;Starting Act_runner &amp;ldquo;rootless&amp;rdquo;&lt;/h3&gt;&#10;&lt;p&gt;So I follow the instructions and copy together a suitable &lt;code&gt;docker-compose.yml&lt;/code&gt;. Important here:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;code&gt;privileged: true&lt;/code&gt; must be set. Otherwise, the Docker daemon in the &lt;em&gt;act_runner&lt;/em&gt; container cannot start properly because it lacks kernel functions. This causes the entire container to crash repeatedly without generating any helpful error messages.&lt;/li&gt;&#10;&lt;li&gt;The environment variable &lt;code&gt;DOCKER_HOST=unix:///var/run/user/1000/docker.sock&lt;/code&gt; must be set. Here, the Docker socket is controlled by a non-privileged user and is available to the runner for managing job containers.&#10;The daemon runs encapsulated in the container and is not connected to the host machine.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="as-of-jul-2025-problem-with-0212-dind-rootless"&gt;As of Jul-2025: Problem with 0.2.12-dind-rootless&lt;/h3&gt;&#10;&lt;p&gt;This is where I encountered my first problem. The runner crashes shortly after starting with the following error message:&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;schallbert@machine:~# docker logs gitea-runner&#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;[&lt;/span&gt;rootlesskit:parent&lt;span style="color:#f92672"&gt;]&lt;/span&gt; error: failed to start the child: fork/exec /proc/self/exe: operation not permitted&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Help came from the &lt;a href="https://gitea.com/gitea/act_runner/issues/721" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Gitea community&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. &lt;em&gt;act_runner&lt;/em&gt; runs smoothly with the previous version &lt;strong&gt;0.2.11&lt;/strong&gt;. However, for reasons unknown to me, it is displayed on Gitea as 0.2.12.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-08-10-gitea-act-runner-dind-rootless.avif" alt="Image: DinD-rootless runner is now working fine"&gt;&lt;/figure&gt;&#10;&lt;h3 id="volume-confusion"&gt;Volume Confusion&lt;/h3&gt;&#10;&lt;p&gt;Great! Now that the runner is working, I&amp;rsquo;ll let it run a job right away. Unfortunately, the build fails after just a fraction of a second with this message:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;# Runner step: Set up job&#10;failed to start container: Error response from daemon: error while creating mount source path &amp;#39;&amp;lt;volumeSourceFullPath&amp;gt;&amp;#39;: mkdir &amp;lt;volumeSourcePath&amp;gt;: permission denied&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I had to research this for hours and was under the false impression for a long time that it was due to insufficient permissions on the folders on the host machine. Only later did I truly understand that Docker-in-Docker means exactly what it says: Not only are containers created by containers, but a separate Docker daemon runs within the container!&lt;/p&gt;&#10;&lt;p&gt;This means that the classic method of making volumes in job containers available directly from the host system using &lt;code&gt;-v /a/b:/x/y&lt;/code&gt; no longer works.&#10;Instead, volumes must now be passed through. Example:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&lt;em&gt;host&lt;/em&gt; directory &lt;code&gt;/opt/server/www/blog-artifacts&lt;/code&gt; -&amp;gt; &lt;em&gt;act_runner&lt;/em&gt; volume &lt;code&gt;/tmp/blog-artifacts&lt;/code&gt; &amp;ndash;&amp;gt; &lt;em&gt;job&lt;/em&gt; container volume &lt;code&gt;/tmp/blog-artifacts&lt;/code&gt;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;The &lt;code&gt;:z&lt;/code&gt; in the &lt;em&gt;act_runner&lt;/em&gt; volume is now important. It indicates to &lt;em&gt;Docker&lt;/em&gt; that this volume is shared between containers. These volumes must not only be specified in &lt;em&gt;act_runner&lt;/em&gt;&amp;rsquo;s &lt;code&gt;docker-compose.yml&lt;/code&gt;, but the job scripts in the &lt;code&gt;.gitea/workflows/&lt;/code&gt; folder must also be adjusted accordingly. However, the &lt;code&gt;:z&lt;/code&gt; on the &amp;ldquo;right side&amp;rdquo; is not needed here.&lt;/p&gt;&#10;&lt;h3 id="is-not-a-valid-volume"&gt;&amp;ldquo;is not a valid volume&amp;rdquo;&lt;/h3&gt;&#10;&lt;p&gt;Despite all my efforts, my jobs still aren&amp;rsquo;t running. This time, because of an error message that &lt;a href="https://blog.schallbert.de/en/gitea-action-runner-jekyll-dockerimage/#action-volumes"&gt;already seemed familiar&lt;/a&gt;:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log*" data-lang="log*"&gt;# Runner step: Set up job&#10;[/tmp/blog-artifacts] is not a valid volume, will be ignored&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So, go into the &lt;code&gt;config.yml&lt;/code&gt; of &lt;em&gt;act_runner&lt;/em&gt; and add the volume names:&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/runner/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:#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;valid_volumes&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;/tmp/blog-artifacts&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;/tmp/lectures-artifacts&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:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In this file, I can leave &lt;code&gt;privileged: false&lt;/code&gt; because, unlike &lt;em&gt;act_runner&lt;/em&gt;, the job container doesn&amp;rsquo;t require kernel features.&lt;/p&gt;&#10;&lt;h2 id="set-permissions-correctly"&gt;Set permissions correctly&lt;/h2&gt;&#10;&lt;p&gt;Now I&amp;rsquo;m getting &lt;code&gt;Permission Denied&lt;/code&gt; error messages again when running my jobs, although not directly in the first step of the actions. Since I&amp;rsquo;ve now checked the volume paths down to the last detail, it can only be due to the folder permissions on the host machine.&lt;/p&gt;&#10;&lt;p&gt;In order for the artifacts created by the job container to be stored via the volumes on my host, I have to pass the directory to be written and all subfolders &lt;code&gt;-R&lt;/code&gt; to the previously defined, non-privileged user &lt;code&gt;ID=1000&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-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# chown -R 1000:1000 /target/path/to/artifact/&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Finally, everything is working smoothly, and I&amp;rsquo;ve put a stop to the (unlikely, but possible) takeover of my host system by malicious job containers.&lt;/p&gt;&#10;</description></item><item><title>Add rate limiter to Gitea</title><link>https://blog.schallbert.de/en/gitea-rate-limiter/</link><pubDate>Fri, 25 Jul 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/gitea-rate-limiter/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-07-25-gitea-rate-limiter-thumb.avif"&#10; class="post-cover"&#10; alt="Image: Gitea&amp;#39;&amp;#39;s logo, a cup of tea, protected by many arrows symbolizing GET requests"&#10; title="Add rate limiter to Gitea" /&gt;&#10;&lt;p&gt;Here, I&amp;rsquo;m providing a step-by-step guide to solving my &lt;a href="https://blog.schallbert.de/en/gitea-out-of-memory/"&gt;&lt;em&gt;Gitea&lt;/em&gt; crashes&lt;/a&gt; problem. I&amp;rsquo;m incorporating the experience I gained from using a rate limiter &lt;a href="https://blog.schallbert.de/en/fail2ban-with-caddy/"&gt;for my blog&lt;/a&gt; with the goal of making the application more robust and protected against undirected denial of service attacks.&lt;/p&gt;&#10;&lt;h2 id="step-1-specify-the-log-source-for-the-rate-limiter"&gt;Step 1: Specify the log source for the rate limiter&lt;/h2&gt;&#10;&lt;p&gt;&lt;em&gt;Gitea&lt;/em&gt; naturally generates logs itself, theoretically down to the access level by external clients. However, in the past, I haven&amp;rsquo;t been able to &lt;a href="https://blog.schallbert.de/en/server-protection/#what-does-not-yet-work-gitea--fail2ban"&gt;export Gitea&amp;rsquo;s access logs from Docker&lt;/a&gt;. Therefore, we now instruct &lt;em&gt;Caddyserver&lt;/em&gt;, which is connected as a reverse proxy, to create logs on behalf of &lt;em&gt;Gitea&lt;/em&gt;. Access to the Gitea web interface will then appear in these logs.&lt;/p&gt;&#10;&lt;p&gt;In the Caddyfile, the log module looks completely unspectacular.&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-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /caddy/Caddyfile&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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;log {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;output file &lt;span style="color:#e6db74"&gt;/log/&lt;/span&gt;gitea&lt;span style="color:#f92672"&gt;/&lt;/span&gt;access&lt;span style="color:#f92672"&gt;.&lt;/span&gt;log&#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="exclude-internal-traffic-from-the-log"&gt;Exclude internal traffic from the log&lt;/h3&gt;&#10;&lt;p&gt;Now I look at the logs and see entries that I don&amp;rsquo;t want. For example, &lt;em&gt;act_runner&lt;/em&gt; generates a &lt;code&gt;Fetch Task POST&lt;/code&gt; to &lt;em&gt;Gitea&lt;/em&gt; every two seconds and a &lt;code&gt;GET&lt;/code&gt; request every ten seconds with the same target as a &lt;a href="https://blog.schallbert.de/en/fix-gitea-runner/"&gt;Health check&lt;/a&gt;. These don&amp;rsquo;t even need to appear in the log for me. My first thought here was to simply not log requests from internal IP addresses. However, since &lt;em&gt;Caddy&lt;/em&gt; acts as a reverse proxy, all IP addresses run &amp;ldquo;internally&amp;rdquo; without exception.&lt;/p&gt;&#10;&lt;p&gt;So I have to recognize &lt;em&gt;act_runner&lt;/em&gt;-specific logs differently:&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-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /caddy/Caddyfile&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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git&lt;span style="color:#f92672"&gt;.&lt;/span&gt;schallbert&lt;span style="color:#f92672"&gt;.&lt;/span&gt;de {&#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; reverse_proxy &lt;span style="color:#f92672"&gt;*&lt;/span&gt; &lt;span style="color:#e6db74"&gt;http&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;//&lt;/span&gt;&lt;span style="color:#e6db74"&gt;gitea&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;3000&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;# Enable logging for fail2ban, don&amp;#39;t log for runner&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log_skip &lt;span style="color:#e6db74"&gt;/api/&lt;/span&gt;actions&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; log {&#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; output file &lt;span style="color:#e6db74"&gt;/log/&lt;/span&gt;gitea&lt;span style="color:#f92672"&gt;/&lt;/span&gt;access&lt;span style="color:#f92672"&gt;.&lt;/span&gt;log&#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;The &lt;a href="https://caddyserver.com/docs/caddyfile/directives/log_skip" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;code&gt;log_skip&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; directive now instructs &lt;em&gt;caddy&lt;/em&gt; to no longer create logs for access to &lt;code&gt;/api/actions*&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="reverse-proxy-displaying-remote-ips"&gt;Reverse Proxy: Displaying Remote IPs&lt;/h3&gt;&#10;&lt;p&gt;However, I still have a problem: If all requests in the log come from an internal IP address, how am I supposed to block &amp;ldquo;bad&amp;rdquo; requests? A web search &lt;a href="https://caddy.community/t/how-to-get-a-true-remote-ip-behind-caddy-reverse-proxy/22348/2" target="_blank" rel="noopener noreferrer" class="external-link"&gt;shows that this is a common problem for reverse proxies in Docker&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Unfortunately, many solutions cannot be applied to this situation because they use different server software like &lt;em&gt;nginx&lt;/em&gt; or have different services running behind their proxies than &lt;em&gt;Gitea.&lt;/em&gt;&lt;/p&gt;&#10;&lt;p&gt;But it&amp;rsquo;s actually quite simple: Just insert a line in the correct place in the &lt;code&gt;Caddyfile&lt;/code&gt;, and the remote IP addresses come in unchanged.&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-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /caddy/Caddyfile&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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git&lt;span style="color:#f92672"&gt;.&lt;/span&gt;schallbert&lt;span style="color:#f92672"&gt;.&lt;/span&gt;de {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; reverse_proxy &lt;span style="color:#f92672"&gt;*&lt;/span&gt; &lt;span style="color:#e6db74"&gt;http&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;//&lt;/span&gt;&lt;span style="color:#e6db74"&gt;gitea&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;3000&lt;/span&gt; {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; trusted_proxies &lt;span style="color:#ae81ff"&gt;172&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&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;0&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt; &lt;span style="color:#75715e"&gt;# Docker-internal netwock traffic runs with these IPs&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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;a href="https://caddyserver.com/docs/caddyfile/options#trusted-proxies" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;code&gt;trusted_proxies&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; directive tells &lt;em&gt;Caddy&lt;/em&gt; that the network address range provided by &lt;em&gt;Docker&lt;/em&gt; can be trusted. Now the actual source IP address is shown instead of the container&amp;rsquo;s internal interface address. This is exactly what I need to be able to analyze the addresses later with &lt;em&gt;fail2ban&lt;/em&gt;.&lt;/p&gt;&#10;&lt;h2 id="step-2-determine-the-use-case"&gt;Step 2: Determine the Use Case&lt;/h2&gt;&#10;&lt;p&gt;Let&amp;rsquo;s look at how many HTTP &lt;code&gt;200 ok&lt;/code&gt; requests are coming in the edge case between normal use and &amp;ldquo;abuse.&amp;rdquo; To do this, I surf around &lt;em&gt;Gitea&lt;/em&gt; and click on a bunch of things that I would never normally do at that speed as a human. Then I analyze the logs.&lt;/p&gt;&#10;&lt;p&gt;This gives us some initial benchmarks for the rate limiter.&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;# rate limiter tests&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;findtime = 10s&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;maxretry = 10&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;bantime = 6h&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="step-3-configure-fail2ban"&gt;Step 3: Configure Fail2ban&lt;/h2&gt;&#10;&lt;p&gt;Next, we configure the filter and jail file of &lt;code&gt;fail2ban&lt;/code&gt; to analyze the logs defined above.&lt;/p&gt;&#10;&lt;h3 id="filter"&gt;Filter&lt;/h3&gt;&#10;&lt;p&gt;Here, I&amp;rsquo;m re-using the same filter for my rate limiter from the &lt;a href="https://blog.schallbert.de/en/fail2ban-with-caddy/#directory-filterd"&gt;previous article&lt;/a&gt;. We&amp;rsquo;re only interested in successful requests that we count within a time window.&lt;/p&gt;&#10;&lt;h3 id="jail"&gt;Jail&lt;/h3&gt;&#10;&lt;p&gt;I&amp;rsquo;m using the values from the rate limiter tests 1:1 in the jail file. I&amp;rsquo;m referring to &lt;code&gt;caddy-ratelimit&lt;/code&gt; (link above) as the filter. It is essential to select the &lt;code&gt;DOCKER-USER&lt;/code&gt; chain as the requests are routed via Docker&amp;rsquo;s virtual network.&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;# /fail2ban/config/fail2ban/jail.local&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;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#ae81ff"&gt;gitea-ratelimit]&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;enabled = true&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;chain = DOCKER-USER&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;port = http,https&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;filter = caddy-ratelimit&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;logpath = /var/log/caddy2/gitea/access.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;findtime = 10s&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;maxretry = 10&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;bantime = 6h&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I reboot &lt;em&gt;fail2ban&lt;/em&gt; to activate the protection.&lt;/p&gt;&#10;&lt;h2 id="step-4-test-the-rate-limiter"&gt;Step 4: Test the rate limiter&lt;/h2&gt;&#10;&lt;p&gt;I proceed exactly as in my article &lt;a href="https://blog.schallbert.de/en/fail2ban-with-caddy/#test-rate-limiter"&gt;fail2ban with caddy&lt;/a&gt; and get the same result. It works!&lt;/p&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Fix &amp;#39;Bad value substitution&amp;#39; error&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2025-10-09T00:00:00Z"&gt;&#10; 2025-10-09&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; There is an important update that fixes fail2ban&amp;rsquo;s &lt;a href="https://blog.schallbert.de/en/fail2ban-error-configuration-bad-value/"&gt;Error: &amp;lsquo;Bad value substitution&amp;rsquo; for &amp;lsquo;action&amp;rsquo;&lt;/a&gt;. This problem emerges when &lt;em&gt;fail2ban&lt;/em&gt; is trying services that run in a Docker container environment.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;</description></item><item><title>Rate limiter with Caddy and fail2ban</title><link>https://blog.schallbert.de/en/fail2ban-with-caddy/</link><pubDate>Thu, 10 Jul 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/fail2ban-with-caddy/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-07-10-fail2ban-ratelimit-thumb.avif"&#10; class="post-cover"&#10; alt="Image: block drawing of how caddy interacts with fail2ban to rate limit accesses to my blog"&#10; title="Rate limiter with Caddy and fail2ban" /&gt;&#10;&lt;p&gt;Here I describe how to configure &lt;em&gt;fail2ban&lt;/em&gt; (commonly used to &lt;a href="https://blog.schallbert.de/en/server-protection/"&gt;defend against unauthorized access attempts&lt;/a&gt;) to limit the number of successful accesses within a defined time window. This is called &amp;ldquo;rate limiting&amp;rdquo; and is intended to thwart denial of service attacks that overload my server.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-07-10-fail2ban-ratelimit.avif" alt="Image: block drawing of how caddy interacts with fail2ban to rate limit accesses to my blog"&gt;&lt;/figure&gt;&#10;&lt;h2 id="enabling-caddy-logs"&gt;Enabling Caddy Logs&lt;/h2&gt;&#10;&lt;p&gt;First, we need to enable the web server&amp;rsquo;s logger. With Caddy, this can be done with just a few lines of code that I add to my &lt;a href="https://blog.schallbert.de/en/projects/move-blog-to-own-server/#caddy"&gt;existing configuration&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-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# caddy2/config/Caddyfile&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:#ae81ff"&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:#75715e"&gt;# Define webserver&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;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:#ae81ff"&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:#ae81ff"&gt;file_server&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;# Enable logging for fail2ban&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 {&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;output file /log/blog/access.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;&#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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;From now on, logs will be created in the specified path, which I manage with a &lt;a href="https://blog.schallbert.de/en/logrotate-mistake/"&gt;&lt;em&gt;logrotate&lt;/em&gt; configuration&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="configuring-fail2ban-as-a-rate-limiter"&gt;Configuring Fail2ban as a Rate Limiter&lt;/h2&gt;&#10;&lt;p&gt;To recap: &lt;em&gt;fail2ban&lt;/em&gt; accesses the machine&amp;rsquo;s packet filter rules and, in effect, modifies its firewall to ward off dynamic attacks. For this to work, &lt;em&gt;fail2ban&lt;/em&gt; must be provided with log files generated by a host application, such as a web server, that contain the IP addresses of the client computers.&lt;/p&gt;&#10;&lt;h3 id="preliminary-consideration-http-200-ok-as-a-filter"&gt;Preliminary consideration: &amp;ldquo;http 200 OK&amp;rdquo; as a filter?&lt;/h3&gt;&#10;&lt;p&gt;Even normal, permitted accesses &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/200" target="_blank" rel="noopener noreferrer" class="external-link"&gt;200 OK&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; must count for my rate limiter.&lt;/p&gt;&#10;&lt;p&gt;To get a feel for how my logger records accesses, I take a look at the logs. I get the number of entries that contain a status of &lt;code&gt;200 OK&lt;/code&gt; returned. To do this, I perform a search with &lt;em&gt;grep&lt;/em&gt; and calculate the number of hits line by line using &lt;code&gt;wc -l&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-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:/var/log/caddy2/blog# grep -o &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#34;status&amp;#34;:200&amp;#39;&lt;/span&gt; access.log | wc -l&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;847&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Okay, so there have already been 847 hits today, which received a &lt;code&gt;200 OK&lt;/code&gt; response. Now I&amp;rsquo;ll call up my blog&amp;rsquo;s landing page and display the article overview.&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;schallbert@machine:/var/log/caddy2/blog# grep -o &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#34;status&amp;#34;:200&amp;#39;&lt;/span&gt; access.log | wc -l&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;858&lt;/span&gt; // after calling blog.schallbert.de&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:/var/log/caddy2/blog# grep -o &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#34;status&amp;#34;:200&amp;#39;&lt;/span&gt; access.log | wc -l&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;882&lt;/span&gt; // page loaded after clicking &lt;span style="color:#e6db74"&gt;&amp;#34;Posts&amp;#34;&lt;/span&gt; button&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Wow, that was 35 entries for two clicks! Looking at the log, it explains the many lines of assets—images and logos—that are being loaded.&lt;/p&gt;&#10;&lt;p&gt;Unfortunately, this makes it clear that the filter criterion &lt;code&gt;status:200&lt;/code&gt; for my rate limiter can&amp;rsquo;t work without further ado. The number of notifications depends largely on the respective article. So, I would have to define a threshold above which no normal person would generate access to my blog.&lt;/p&gt;&#10;&lt;p&gt;In order to count only &amp;ldquo;real&amp;rdquo; accesses to my pages, I have to somehow exclude the assets from the logs. Fortunately, there&amp;rsquo;s a simple &lt;a href="https://caddyserver.com/docs/caddyfile/directives/log_skip" target="_blank" rel="noopener noreferrer" class="external-link"&gt;directive in Caddy&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; for this.&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-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# caddy2/config/Caddyfile&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;# Enable logging for fail2ban&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log_skip &lt;span style="color:#e6db74"&gt;/assets*&#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; log {&#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; output file /&lt;/span&gt;log&lt;span style="color:#f92672"&gt;/&lt;/span&gt;blog&lt;span style="color:#f92672"&gt;/&lt;/span&gt;access&lt;span style="color:#f92672"&gt;.&lt;/span&gt;log&#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;log_skip&lt;/code&gt; now ensures that all accesses to files in the &lt;code&gt;assets/&lt;/code&gt; folder and below are not logged.&lt;/p&gt;&#10;&lt;p&gt;The behavior of Fail2ban is defined using two configuration files:&lt;/p&gt;&#10;&lt;h3 id="directory-filterd"&gt;Directory &lt;code&gt;filter.d&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;&lt;code&gt;filter.d/&amp;lt;filter-name&amp;gt;.conf&lt;/code&gt; contains the definition of an event to be monitored. Thus, the &lt;code&gt;failregex&lt;/code&gt; &amp;ldquo;page not found&amp;rdquo; &lt;code&gt;http 404&lt;/code&gt; status code can be used to trigger the packet filter, just as successful access can be used to set up my rate limiter, which responds to &lt;code&gt;200 OK&lt;/code&gt;. I copied the regex from &lt;a href="https://www.kassner.com.br/en/2023/09/10/fail2ban-caddy-json-logs/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Rafael Kassner&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-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /opt/fail2ban/config/filter.d/caddy-ratelimit.conf&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:#ae81ff"&gt;Definition]&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;failregex = &amp;#34;client_ip&amp;#34;:&amp;#34;&amp;lt;HOST&amp;gt;&amp;#34;(.*)&amp;#34;status&amp;#34;:200&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;datepattern = \d+&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;ignoreregex =&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In addition, further boundary conditions are defined in the file, e.g., the date format &lt;code&gt;datepattern&lt;/code&gt; is adapted to the log output of the system to be protected.&lt;/p&gt;&#10;&lt;h3 id="jaillocal-file"&gt;&lt;code&gt;jail.local&lt;/code&gt; file&lt;/h3&gt;&#10;&lt;p&gt;&lt;code&gt;jail.local&lt;/code&gt; determines the conditions under which the packet filter for the client IP becomes active and blocks further access attempts. For use as a rate limiter, I need the following fields:&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;findtime&lt;/code&gt; - the time window in which attacks are counted, &lt;code&gt;maxretry&lt;/code&gt; is the number of permissible attempts in the time window, and &lt;code&gt;bantime&lt;/code&gt; is the time for which the attack is blocked. &lt;code&gt;ignoreip&lt;/code&gt; is usually preconfigured to the relevant internal IP addresses by default. In my case, &lt;em&gt;act_runner&lt;/em&gt;, for example, sends a message to &lt;em&gt;gitea&lt;/em&gt; internally to query whether any new automation tasks are pending. I definitely don&amp;rsquo;t want to interfere with these accesses.&lt;/p&gt;&#10;&lt;p&gt;The example for &lt;em&gt;fail2ban&lt;/em&gt; used below shows the rate limiter on my blog. In the end, the files and log entries for &lt;em&gt;gitea&lt;/em&gt; are essentially the same. Only the filter names and parameters differ due to the different requirements of the website and the DevOps platform.&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;# /opt/fail2ban/jail.local&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;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#ae81ff"&gt;caddy-ratelimit]&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;enabled = true&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;chain = DOCKER-USER&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;action = iptables-multiport&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;port = http,https&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;filter = caddy-ratelimit&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;logpath = /var/log/caddy2/blog/access.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;findtime = 20s&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;maxretry = 10&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;bantime = 6h&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once these two files are configured, you can restart &lt;em&gt;fail2ban&lt;/em&gt; and view the logs. If there are still errors in the files, &lt;em&gt;fail2ban&lt;/em&gt; will output the following:&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;ERROR Errors in jail &lt;span style="color:#e6db74"&gt;&amp;#39;caddy-ratelimit&amp;#39;&lt;/span&gt;. Skipping...&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If this occurs, for example, the jail cannot be assigned to the filter (must have the same name!), a field definition such as &lt;code&gt;maxretry&lt;/code&gt; is incorrectly typed, or a number format cannot be read. If everything is working correctly, the entry will be:&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;Reading config files: /etc/fail2ban/filter.d/caddy-ratelimit.conf&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="test-rate-limiter"&gt;Testing the Rate Limiter&lt;/h2&gt;&#10;&lt;p&gt;Now I want to check if everything is working as expected.&lt;/p&gt;&#10;&lt;h3 id="checking-the-filter-regex"&gt;Checking the Filter Regex&lt;/h3&gt;&#10;&lt;p&gt;The first step is to look at the filter term and check whether it can be reliably found in the logs. Conveniently, &lt;em&gt;fail2ban&lt;/em&gt; has a built-in tool for this: &lt;code&gt;fail2ban-regex &amp;lt;logfile&amp;gt; &amp;lt;filter&amp;gt;&lt;/code&gt;. So I can simply enter the filter file and a test log file and see if I get any matches.&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;fail2ban-regex ./access.log /etc/fail2ban/filter.d/caddy-ratelimit.conf &#10;&#10;Running tests&#10;=============&#10;&#10;Use filter file : caddy-ratelimit, basedir: /etc/fail2ban&#10;Use datepattern : \d+ : \d+&#10;Use log file : ./access.log.2&#10;Use encoding : UTF-8&#10;&#10;Results&#10;=======&#10;&#10;Failregex: 2009 total&#10;|- #) [# of hits] regular expression&#10;| 1) [2009] &amp;#34;client_ip&amp;#34;:&amp;#34;&amp;lt;HOST&amp;gt;&amp;#34;(.*)&amp;#34;status&amp;#34;:200&#10;`-&#10;&#10;Ignoreregex: 0 total&#10;&#10;Date template hits:&#10;|- [# of hits] date format&#10;| [2685] \d+&#10;`-&#10;&#10;Lines: 2685 lines, 0 ignored, 2009 matched, 676 missed&#10;[processed in 0.12 sec]&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Quantitatively, I get what I expect: the search procedure described above using &lt;em&gt;grep&lt;/em&gt; gives me exactly the same results.&lt;/p&gt;&#10;&lt;h3 id="check-jail"&gt;Check Jail&lt;/h3&gt;&#10;&lt;p&gt;The next step is to test whether the rate limiter is working. To do this, I simulate log entries. I significantly ease jail rules for this test, otherwise I&amp;rsquo;ll have to generate too many entries in a short period of time.&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;# /opt/fail2ban/jail.local TEST&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;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;&lt;span style="color:#ae81ff"&gt;findtime = 10s&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;maxretry = 2&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;bantime = 100s&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After restarting &lt;em&gt;fail2ban&lt;/em&gt;, I create a second console where I can simulate log entries. This must include at least the time, IP address of the caller, and status; however, I play it safe and use complete entries.&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;echo &lt;span style="color:#e6db74"&gt;&amp;#39;{&amp;#34;ts&amp;#34;:1751246139,&amp;#34;remote_ip&amp;#34;:&amp;#34;1.1.1.1&amp;#34;,&amp;#34;status&amp;#34;:200,[superLongIrrelevantOtherStuffForFiltering]}&amp;#39;&lt;/span&gt; &amp;gt;&amp;gt; /var/log/caddy/blog/access.log&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I enter this multiple times to exceed my limit of 2 retries.&lt;/p&gt;&#10;&lt;p&gt;In the first console instance, I then check the &lt;em&gt;fail2ban&lt;/em&gt; status for the corresponding filter.&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;fail2ban-client status caddy-ratelimit&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Status &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; the jail: caddy-ratelimit&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;|- Filter&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;| |- Currently failed: &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;| |- Total failed: &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:#e6db74"&gt;`&lt;/span&gt;- File list: /var/log/caddy2/blog/access.log&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;- Actions&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;|- Currently banned: &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;|- Total banned: &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:#e6db74"&gt;`&lt;/span&gt;- Banned IP list:&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That was nothing. The fail2ban log file reveals why:&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;&lt;span style="color:#75715e"&gt;# /fail2ban/config/log/fail2ban/fail2ban.log&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;WARN &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-ratelimit&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Ignoring all log entries older than 20s; &lt;span style="color:#75715e"&gt;# probably messages generated within a fail2ban restart period&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So again with appropriate timestamps. I have to get these from the current system time (&lt;code&gt;$(date +%s.%N)&lt;/code&gt;) if I have to constantly adjust it. Therefore, I take a complete log line, modify it to the IP address &lt;code&gt;1.1.1.1&lt;/code&gt;, and insert appropriate timestamps:&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-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;echo&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;{\&amp;#34;level\&amp;#34;:\&amp;#34;info\&amp;#34;,\&amp;#34;ts\&amp;#34;:$(date +%s.%N),\&amp;#34;logger\&amp;#34;:\&amp;#34;http.log.access.log0\&amp;#34;,\&amp;#34;msg\&amp;#34;:\&amp;#34;handled request\&amp;#34;,\&amp;#34;request\&amp;#34;:{\&amp;#34;remote_ip\&amp;#34;:\&amp;#34;1.1.1.1\&amp;#34;,\&amp;#34;remote_port\&amp;#34;:\&amp;#34;38229\&amp;#34;,\&amp;#34;client_ip\&amp;#34;:\&amp;#34;1.1.1.1\&amp;#34;,\&amp;#34;proto\&amp;#34;:\&amp;#34;HTTP/1.1\&amp;#34;,\&amp;#34;method\&amp;#34;:\&amp;#34;GET\&amp;#34;,\&amp;#34;host\&amp;#34;:\&amp;#34;blog.schallbert.de\&amp;#34;,\&amp;#34;uri\&amp;#34;:\&amp;#34;/vacuum-clamping/\&amp;#34;,\&amp;#34;headers\&amp;#34;:{\&amp;#34;Accept-Encoding\&amp;#34;:[\&amp;#34;gzip, deflate, br\&amp;#34;],\&amp;#34;Connection\&amp;#34;:[\&amp;#34;keep-alive\&amp;#34;],\&amp;#34;User-Agent\&amp;#34;:[\&amp;#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0\&amp;#34;],\&amp;#34;Accept\&amp;#34;:[\&amp;#34;text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\&amp;#34;],\&amp;#34;Accept-Language\&amp;#34;:[\&amp;#34;en-US,en;q=0.5\&amp;#34;]},\&amp;#34;tls\&amp;#34;:{\&amp;#34;resumed\&amp;#34;:false,\&amp;#34;version\&amp;#34;:772,\&amp;#34;cipher_suite\&amp;#34;:4865,\&amp;#34;proto\&amp;#34;:\&amp;#34;http/1.1\&amp;#34;,\&amp;#34;server_name\&amp;#34;:\&amp;#34;blog.schallbert.de\&amp;#34;}},\&amp;#34;bytes_read\&amp;#34;:0,\&amp;#34;user_id\&amp;#34;:\&amp;#34;\&amp;#34;,\&amp;#34;duration\&amp;#34;:0.001207354,\&amp;#34;size\&amp;#34;:12290,\&amp;#34;status\&amp;#34;:200,\&amp;#34;resp_headers\&amp;#34;:{\&amp;#34;Last-Modified\&amp;#34;:[\&amp;#34;Tue, 10 Jun 2025 19:33:01 GMT\&amp;#34;],\&amp;#34;Content-Encoding\&amp;#34;:[\&amp;#34;gzip\&amp;#34;],\&amp;#34;Server\&amp;#34;:[\&amp;#34;Caddy\&amp;#34;],\&amp;#34;Alt-Svc\&amp;#34;:[\&amp;#34;h3=\\\&amp;#34;:443\\\&amp;#34;; ma=2592000\&amp;#34;],\&amp;#34;Vary\&amp;#34;:[\&amp;#34;Accept-Encoding\&amp;#34;],\&amp;#34;Etag\&amp;#34;:[\&amp;#34;\\\&amp;#34;gzip\\\&amp;#34;\&amp;#34;],\&amp;#34;Content-Type\&amp;#34;:[\&amp;#34;text/html; charset=utf-8\&amp;#34;]}}&amp;#34;&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;access.log&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After calling this command several times, I now get&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;Status &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; the jail: caddy-blog-ratelimit&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;|- Filter&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;| |- Currently failed:&#9;&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;| |- Total failed:&#9;&lt;span style="color:#ae81ff"&gt;11&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;`&lt;/span&gt;- File list:&#9;/var/log/caddy2/blog/access.log&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;- Actions&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; |- Currently banned:&#9;&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; |- Total banned:&#9;&lt;span style="color:#ae81ff"&gt;4&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;`&lt;/span&gt;- Banned IP list:&#9;1.1.1.1&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That looks great.&lt;/p&gt;&#10;&lt;h3 id="check-packet-filter"&gt;Check packet filter&lt;/h3&gt;&#10;&lt;p&gt;Phew, finally done! This is how &lt;code&gt;fail2ban.log&lt;/code&gt; looks like right now. It shows &lt;code&gt;[caddy-ratelimit]&lt;/code&gt; entries as expected:&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;schallbert@machine:~# nano /fail2ban/config/log/fail2ban/fail2ban.log&#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;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-ratelimit&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP a&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-status&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP b&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-ratelimit&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Ignore 172.18.0.1 by ip&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-status&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Ignore 172.18.0.1 by ip&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-ratelimit&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP c&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-ratelimit&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP c&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-status&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP c&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-status&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP c&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-ratelimit&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP d&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-status&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP d&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-status&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP e&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-ratelimit&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP e&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-ratelimit&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP f&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To test the entire chain, we now need to check whether the corresponding IP address is actually blocked in the hardware. To do this, I enter:&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;schallbert@machine:~# iptables -n -L | grep &lt;span style="color:#e6db74"&gt;&amp;#34;1.1.1.1&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;REJECT all -- 1.1.1.1 0.0.0.0/0 reject-with icmp-port-unreachable&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Everything seems to be fine. For one final test from an external IP address, I call a &lt;a href="https://www.deadlinkchecker.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Link Checker&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; in the browser, which should crawl my website and thus trigger the rate limiter. Although it crawls 44 links in a very short time, the test is displayed as &amp;ldquo;passed.&amp;rdquo; Very strange, &lt;code&gt;[caddy-ratelimit]&lt;/code&gt; should have triggered!&lt;/p&gt;&#10;&lt;h3 id="iptable-chain-input-instead-of-forward"&gt;iptable-chain INPUT instead of FORWARD&lt;/h3&gt;&#10;&lt;p&gt;Confused, I take a look at the entire &lt;code&gt;iptable&lt;/code&gt;. At the same time, I look up the IP address of the link checker. It appears in the &lt;code&gt;iptables&lt;/code&gt;. And yet, it&amp;rsquo;s apparently not blocked. Why is that? A closer look shows:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;Chain INPUT (policy ACCEPT)&#10;target prot opt ​​source destination&#10;f2b-caddy-ratelimit tcp -- 0.0.0.0/0 0.0.0.0/0 multiport dports 22&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The problem here is that my jail is on the &lt;code&gt;INPUT&lt;/code&gt; chain. However, the requests don&amp;rsquo;t go directly to my server hardware, but are forwarded via &lt;em&gt;Docker&lt;/em&gt; to &lt;em&gt;Caddyserver&lt;/em&gt;. To work, I have to land on the &lt;code&gt;FORWARD&lt;/code&gt; chain, where &lt;code&gt;DOCKER-USER&lt;/code&gt; already is. Very strange, since I had specifically specified &lt;code&gt;chain = DOCKER-USER&lt;/code&gt; in &lt;code&gt;jail.local&lt;/code&gt;. Something must be overriding this definition.&lt;/p&gt;&#10;&lt;p&gt;An inconspicuous &lt;a href="https://gist.github.com/Rankarusu/23a04ed587b05c6f2b701f2457a127b0?permalink_comment_id=5347631#gistcomment-5347631" target="_blank" rel="noopener noreferrer" class="external-link"&gt;forum post&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; together with the &lt;code&gt;multiport&lt;/code&gt; comment in the iptables printout led me to the solution: The &lt;code&gt;action = iptables-multiport&lt;/code&gt; directive overrides my &lt;code&gt;chain = DOCKER-USER&lt;/code&gt; statement, because the following is set in the corresponding configuration file &lt;code&gt;iptables.conf&lt;/code&gt;: &lt;code&gt;chain = INPUT&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;So I simply delete the &lt;code&gt;action&lt;/code&gt; entry, so that &lt;em&gt;fail2ban&lt;/em&gt; reverts to the default for &lt;code&gt;DOCKER-USER&lt;/code&gt;: &lt;code&gt;multiport dports 80,443&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Now the jail name also appears correctly in &lt;code&gt;iptables&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-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# iptables -n -L&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Chain DOCKER-USER &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; references&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;target prot opt ​​source destination&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;f2b-caddy-ratelimit tcp -- 0.0.0.0/0 0.0.0.0/0 multiport dports 80,443&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Finally, another link check shows that my rate limiter is working.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-07-10-deadlinkcheck-ratelimit-working.avif" alt="Image: deadlinkchecker view for blog.schallbert.de hits configured rate limit and gets blocked subsequently. Thus it returns a Timeout"&gt;&lt;/figure&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Fix &amp;#39;Bad value substitution&amp;#39; error&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2025-10-09T00:00:00Z"&gt;&#10; 2025-10-09&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; There is an important update that fixes &lt;em&gt;fail2ban&lt;/em&gt;&amp;rsquo;s &lt;a href="https://blog.schallbert.de/en/fail2ban-error-configuration-bad-value/"&gt;Error: &amp;lsquo;Bad value substitution&amp;rsquo; for &amp;lsquo;action&amp;rsquo;&lt;/a&gt;. This problem can emerge when &lt;em&gt;fail2ban&lt;/em&gt; is protecting services that run in a Docker container environment.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;</description></item><item><title>Gitea crashes: Too many requests</title><link>https://blog.schallbert.de/en/gitea-out-of-memory/</link><pubDate>Mon, 30 Jun 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/gitea-out-of-memory/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-06-30-server-load-thumb.avif"&#10; class="post-cover"&#10; alt="Image: server at max load when gitea is flooded with GET requests"&#10; title="Gitea crashes: Too many requests" /&gt;&#10;&lt;p&gt;Here I&amp;rsquo;d like to briefly describe how a service crash repeatedly paralyzed my server for hours. So badly that I could only restart it from the provider&amp;rsquo;s console. I&amp;rsquo;ll explain how it happened and how I intend to avoid this and similar problems in the future.&lt;/p&gt;&#10;&lt;h2 id="i-was-attacked-or-was-i"&gt;I was attacked. Or was I?&lt;/h2&gt;&#10;&lt;p&gt;I was working on an article that I wanted to post later. To be on the safe side in terms of backup, I created a commit as usual and wanted to push it to my &lt;em&gt;Gitea&lt;/em&gt; instance. But my &lt;code&gt;git push&lt;/code&gt; command simply didn&amp;rsquo;t work.&lt;/p&gt;&#10;&lt;p&gt;Confused, I tried to access my website. The response was:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-06-30-404-message.avif" alt="Image: A browser&amp;#39;s timeout error message `Could not complete your request`"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Strange. Then I wanted to log in to my server to check everything was OK: &lt;code&gt;ssh &amp;lt;servername&amp;gt;&lt;/code&gt;. Again, the terminal remained unresponsive. Bummer!&lt;/p&gt;&#10;&lt;p&gt;Timeout. As a last resort, I logged in to my hoster&amp;rsquo;s web interface and looked at the server&amp;rsquo;s graphs:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-06-30-server-load.avif" alt="Image: 200% CPU load on my server for nearly two hours"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Oh, what&amp;rsquo;s going on? I&amp;rsquo;m trying to shut down the server via the web interface. That doesn&amp;rsquo;t work either. Only a hard reboot succeeds.&#10;I can log in again via SSH and see that all &lt;em&gt;Docker&lt;/em&gt; containers are booting up normally.&lt;/p&gt;&#10;&lt;h2 id="what-happened"&gt;What happened?&lt;/h2&gt;&#10;&lt;p&gt;It&amp;rsquo;s a good thing I keep all the logs for a week. This, along with the machine&amp;rsquo;s utilization over time, allows me to reconstruct what happened. At least to some extent.&lt;/p&gt;&#10;&lt;h3 id="which-logs-help"&gt;Which logs help?&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Localize the crash! Find the triggering application in the &lt;code&gt;kern.log&lt;/code&gt; and note the timestamp.&lt;/li&gt;&#10;&lt;li&gt;Are there system-wide effects or other services being affected? Check the &lt;code&gt;syslog&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;If you suspect the system may have been hacked, &lt;code&gt;auth.log&lt;/code&gt; has the details.&lt;/li&gt;&#10;&lt;li&gt;If the affected application is running in a container, the relevant logs there may be helpful.&lt;/li&gt;&#10;&lt;li&gt;Review the application&amp;rsquo;s logs. The time just before the crash is particularly interesting.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;And here again, in detail, are the logs I reviewed for my behavior and where they can be found.&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;log name&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;relevant content for this issue&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;/var/log/syslog&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;System-wide (bare metal) messages&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;(docker) warning: &amp;ldquo;health check for container &lt;code&gt;&amp;lt;ID&amp;gt;&lt;/code&gt; timeout&amp;rdquo; (containerd) error: &amp;ldquo;ttrpc: received message on inactive stream&amp;rdquo;&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;/var/log/auth.log&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Contains authentication messages from external hosts, in my case mostly SSH&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;None&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;/var/log/kern.log&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Logs all app, service, daemon and system crashes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Out of memory: Killed process &lt;code&gt;&amp;lt;ID&amp;gt;&lt;/code&gt; (&lt;code&gt;&amp;lt;serviceName&amp;gt;&lt;/code&gt;)&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;[.]/gitea/log/gitea.log&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Protocol for requests, actions on Gitea&amp;rsquo;s web interface, repo changes etc.&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;GET&lt;/code&gt; requests, crash/restart indications&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;docker container logs &lt;code&gt;&amp;lt;containerID&amp;gt;&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Docker&amp;rsquo;s logs for the container in question&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Received signal 15; terminating. (SIGTERM)&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="kernlogsyslog"&gt;kern.log/syslog&lt;/h3&gt;&#10;&lt;p&gt;Here, next to the stack trace, I can see exactly what happened. The most understandable message is the following:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;Out of memory: Killed process `&amp;lt;ID&amp;gt;` (gitea)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So, Gitea was unplugged because it had consumed practically all system resources.&#10;If I scroll up in the log, I see a few minutes earlier:&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;&amp;lt;timestamp&amp;gt; &amp;lt;machineName&amp;gt; dockerd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;716&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: level&lt;span style="color:#f92672"&gt;=&lt;/span&gt;warning msg&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;Health check for container &amp;lt;ID&amp;gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;timestamp&amp;gt; &amp;lt;machineName&amp;gt; dockerd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;716&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: level&lt;span style="color:#f92672"&gt;=&lt;/span&gt;error msg&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;stream copy error: reading from a closed fifo&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Already here, I see warnings that the &lt;em&gt;gitea&lt;/em&gt; container isn&amp;rsquo;t working properly. Another advantage of including &amp;ldquo;health checks&amp;rdquo; in the &lt;code&gt;docker-compose.yml&lt;/code&gt; file. For other reasons, I&amp;rsquo;ve already written an article about the purpose and implementation of &lt;a href="https://blog.schallbert.de/en/fix-gitea-runner/#healthcheck"&gt;health checks in &lt;em&gt;docker&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="gitealog"&gt;gitea.log&lt;/h3&gt;&#10;&lt;p&gt;The following log entry indicates that &lt;em&gt;Gitea&lt;/em&gt; has just been restarted due to a bug in a submodule:&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;cmd/web.go:205:serveInstalled&lt;span style="color:#f92672"&gt;()&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;W&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Table system_setting Column version db default is , struct default is &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And further up in the log:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-06-30-attack-log.avif" alt="Image: Gitea log showing numerous GET request entries, many of which for a specific large file (MiB range), following a typical Gitea startup message"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Very interesting. The &lt;code&gt;highlight.css&lt;/code&gt; is in my public repo. It&amp;rsquo;s the scheme for &lt;a href="https://lectures.schallbert.de/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;lectures.schallbert.de&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&amp;rsquo;s appearance. This file is quite large, almost 1 MiB. And it&amp;rsquo;s loaded here dozens of times, practically for every commit.&lt;/p&gt;&#10;&lt;p&gt;Now I&amp;rsquo;m looking at other crashes in the past. It&amp;rsquo;s always bursts of GET commands for large files or requests for compares between two branches of the repository that precede my server crashing.&lt;/p&gt;&#10;&lt;h2 id="whos-behind-this"&gt;Who&amp;rsquo;s behind this?&lt;/h2&gt;&#10;&lt;p&gt;All crash-triggering requests come from the same IP address range. The crashes started a few weeks ago. But mostly at times of day that I(and apparently many of my readers) didn&amp;rsquo;t notice. And after a few minutes, the server was always back to normal operation.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-06-30-gcp-dos.avif" alt="Image: Whois request for the IP that had my server crashed, owner: Google LLC (GCP)"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Oh, the trail leads to Google&amp;rsquo;s Cloud Platform (GCP).&lt;/p&gt;&#10;&lt;h3 id="but-i-had-blocked-robots"&gt;But I had blocked robots?&lt;/h3&gt;&#10;&lt;p&gt;Indeed, I had &lt;a href="https://blog.schallbert.de/gitea-search-indexation/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;stopped the search engine indexing for Gitea&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Strange. Or am I not dealing with one of the Google spiders or &amp;ldquo;AI&amp;rdquo; scrapers, but with a nasty hacker who rented a virtual machine from &amp;ldquo;Google Cloud&amp;rdquo;?&lt;/p&gt;&#10;&lt;h3 id="log-research-how-frequently-is-the-file-requested"&gt;Log research: How frequently is the file requested?&lt;/h3&gt;&#10;&lt;p&gt;A spider would only crawl all my pages once every few weeks, right? And hopefully not ignore my &lt;code&gt;robots.txt&lt;/code&gt;. A crawler certainly wouldn&amp;rsquo;t make the same request multiple times and at short intervals, would it?&lt;/p&gt;&#10;&lt;p&gt;To check this, I search the &lt;em&gt;Gitea&lt;/em&gt; logs for entries of GET requests to one of the large and therefore resource-intensive files to transfer:&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;gunzip gitea.log.&amp;lt;date.rotateID&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;grep &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;lt;filename&amp;gt;&amp;#34;&lt;/span&gt; gitea.log.&amp;lt;date.rotateID&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In the list, I see that the same request to the same target file is made multiple times from the same IP address, all within seconds. Shortly before the crash, my server took almost 4 seconds to serve the request.&lt;/p&gt;&#10;&lt;p&gt;I also see that the IP address changes every few hours.&lt;/p&gt;&#10;&lt;h2 id="countering-a-dos-attack"&gt;Countering a DoS Attack&lt;/h2&gt;&#10;&lt;p&gt;In summary, I have to conclude that I&amp;rsquo;m being attacked via a &lt;a href="https://en.wikipedia.org/wiki/Denial-of-service_attack" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Denial-of-service&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; attack from the GCP address space.&lt;/p&gt;&#10;&lt;p&gt;To gather a bit more background information, I visit a few websites on the topic. There, I learn that &lt;em&gt;gitea&lt;/em&gt; on my server is crashing under an &lt;a href="https://www.geeksforgeeks.org/computer-networks/types-of-dos-attacks/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Application Layer Attack&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Now that I know what&amp;rsquo;s happening and what the problem is, finding solutions is much easier 😅&lt;/p&gt;&#10;&lt;p&gt;Of course, I don&amp;rsquo;t want to give up without a fight by permanently taking my Gitea instance offline. So, what options do I have?&lt;/p&gt;&#10;&lt;h3 id="provide-more-resources"&gt;Provide more resources&lt;/h3&gt;&#10;&lt;p&gt;Admittedly, my machine only has &lt;code&gt;40GB&lt;/code&gt; of memory and &lt;code&gt;2GB&lt;/code&gt; of VRAM, as well as a measly 2-core CPU from 2009. I could book a more powerful server to better handle peak loads. But this wouldn&amp;rsquo;t prevent the attack, only mitigate its effects.&lt;/p&gt;&#10;&lt;h3 id="rate-limiting-directly-in-the-web-server"&gt;Rate limiting directly in the web server&lt;/h3&gt;&#10;&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Rate_limiting" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Rate limiters&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; limit the number of requests a client can make within a certain time window. This conserves server resources. This is a &amp;ldquo;soft&amp;rdquo; defense against DOS attacks, as triggering IP addresses are briefly and gently blocked with an error message. Typically, &lt;code&gt;HTTP status code 429&lt;/code&gt; &amp;ldquo;Too Many Requests&amp;rdquo; is returned when the limiter intervenes.&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;Gitea&lt;/em&gt; doesn&amp;rsquo;t have a rate limiter. In &lt;a href="https://blog.schallbert.de/en/projects/move-blog-to-own-server/#web-access"&gt;my setup&lt;/a&gt;, &lt;em&gt;Gitea&lt;/em&gt; runs behind a reverse proxy provided by my &lt;em&gt;Caddyserver&lt;/em&gt;. So, that&amp;rsquo;s where I&amp;rsquo;d have to start. For &lt;em&gt;Caddy&lt;/em&gt;, rate limiters are only available as &lt;a href="https://caddyserver.com/docs/modules/http.handlers.rate_limit" target="_blank" rel="noopener noreferrer" class="external-link"&gt;external modules&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, which must be manually installed and configured in &lt;em&gt;xcaddy&lt;/em&gt;.&lt;/p&gt;&#10;&lt;h3 id="blocking-with-ip-table-rules"&gt;Blocking with IP-table rules&lt;/h3&gt;&#10;&lt;p&gt;Here, you could again use &lt;em&gt;fail2ban&lt;/em&gt; and simply block multiple requests for the same resource from an IP address. &lt;em&gt;Gitea&lt;/em&gt; has a &lt;a href="https://docs.gitea.com/next/administration/fail2ban-setup" target="_blank" rel="noopener noreferrer" class="external-link"&gt;description of the setup in its documentation&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. In my case, I would have to continue &lt;a href="https://blog.schallbert.de/en/server-protection/#what-does-not-yet-work-gitea--fail2ban"&gt;where I left off&lt;/a&gt;, and thus monitor not only SSH but also normal page requests.&lt;/p&gt;&#10;&lt;p&gt;This solution sounds the most sensible to me, as it clearly separates concerns into different applications. I only use applications I already have available: &lt;em&gt;Caddy&lt;/em&gt; would provide the access logs, and &lt;em&gt;fail2ban&lt;/em&gt; would have to read them and set filters in the &lt;code&gt;jail.local&lt;/code&gt; configuration so that it acts like a rate limiter.&lt;/p&gt;&#10;&lt;p&gt;Let me link the follow-up article &lt;a href="https://blog.schallbert.de/en/fail2ban-with-caddy/"&gt;Setting up Fail2ban with Caddy&lt;/a&gt; here 🙂&lt;/p&gt;&#10;</description></item><item><title>My own webshop - Part3</title><link>https://blog.schallbert.de/en/ecommerce-selection/</link><pubDate>Sun, 15 Jun 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/ecommerce-selection/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-06-15-opencart-thumb.avif"&#10; class="post-cover"&#10; alt="Image: Snip of OpenCart&amp;#39;s article about data safety"&#10; title="My own webshop - Part3" /&gt;&#10;&lt;h2 id="selecting-a-solution-based-on-key-questions"&gt;Selecting a solution based on key questions&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Which solution is straightforward and quick to set up?&lt;/li&gt;&#10;&lt;li&gt;Which solution is easier to maintain?&lt;/li&gt;&#10;&lt;li&gt;With which system can I, as the operator, most easily understand how everything works (transparency, dependencies)?&lt;/li&gt;&#10;&lt;li&gt;Which system has the smallest attack surface?&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="theres-still-quite-a-bit-missing-here"&gt;There’s still quite a bit missing here!&lt;/h2&gt;&#10;&lt;p&gt;Over the last few weeks, my enthusiasm for setting up my own online shop has waned a bit, as I wanted to focus first on securing my server and finishing off some old, nearly completed projects. I’ll return to this series as soon as that ‘I-want-to-do-something-new’ fever takes hold of me again.&lt;/p&gt;&#10;&lt;p&gt;Hang in there!&#10;&lt;em&gt;Schallbert&lt;/em&gt;&lt;/p&gt;&#10;</description></item><item><title>Creating an own Webshop - Part 2"</title><link>https://blog.schallbert.de/en/ecommerce-alternatives/</link><pubDate>Thu, 15 May 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/ecommerce-alternatives/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-05-15-ecommerce-frameworks-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: logos from different e-commerce solutions"&#10; title="Creating an own Webshop - Part 2&amp;#34;" /&gt;&#10;&lt;h2 id="a-few-options-for-webshop-software"&gt;A few options for webshop software&lt;/h2&gt;&#10;&lt;p&gt;Let&amp;rsquo;s assume only software that at least partially meets my &lt;a href="https://blog.schallbert.de/en/ecommerce-requirements/"&gt;requirements&lt;/a&gt; get shortlisted. Below, I would like to present a few possible ecommerce solutions.&lt;/p&gt;&#10;&lt;h3 id="shopify"&gt;Shopify&lt;/h3&gt;&#10;&lt;p&gt;Shopify is an all-inclusive package solution for online retail. Shopify Inc. is headquartered in Canada and handles its European business from Ireland. The &lt;a href="https://www.shopify.com/de/legal" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Legal&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; sections found on the website, as well as the complexity of the information presented on the topic of &lt;a href="https://www.shopify.com/de/legal/impressum?country=de&amp;amp;lang=en" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Data Protection&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, lead me to suspect that Shopify is a multinational corporation – and not just a provider of online shops.&lt;/p&gt;&#10;&lt;p&gt;But back to the topic.&lt;/p&gt;&#10;&lt;p&gt;The domain and hosting are included in the price with Shopify, and you can create your own shop rapidly and without any programming knowledge in a browser window by selecting a theme, and setting a few parameters. Shopify has extensive documentation and offers training and all sorts of other services for its subscribers.&lt;/p&gt;&#10;&lt;p&gt;But where&amp;rsquo;s the fun in that? After all, I run a tech blog and am not a businessman, literally. A bit of programming is definitely something I enjoy. Furthermore, as an open source fan, I don&amp;rsquo;t particularly like the corporate stance (just look at their modern website, equipped with auto-playing videos and optimized for marketing purposes). The configurability is mediocre at best, and, as with most other solutions, I have to do the optimization for search engine results myself. Regarding data protection and security, they maintain the usual compliance and state-of-the-art communication; so I can&amp;rsquo;t look behind it and have to assume that they protect their customers well, also in their own interest.&lt;/p&gt;&#10;&lt;p&gt;Shopify describes itself as a &lt;a href="https://www.shopify.com/blog/open-source-ecommerce#5" target="_blank" rel="noopener noreferrer" class="external-link"&gt;closed-source SaaS platform&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Therefore, I&amp;rsquo;m removing this platform from my list of preferred web shop solutions because I don&amp;rsquo;t want to become unnecessarily dependent on the well-being of third parties. With Shopify, it&amp;rsquo;s not me who has control over subscription costs or &amp;ldquo;my&amp;rdquo; share of the cloud, but Shopify. It&amp;rsquo;s not me who can determine how many transactions are included in the &amp;ldquo;plan,&amp;rdquo; but Shopify. It&amp;rsquo;s not my customers who remain in control of their data, but Shopify or a third-party company they have commissioned to process it.&lt;/p&gt;&#10;&lt;h3 id="wordpress--woocommerce"&gt;Wordpress / WooCommerce&lt;/h3&gt;&#10;&lt;p&gt;I only know Wordpress as a juggernaut. The jack of all trades, capable of everything you could possibly want thanks to an overwhelming mass of plugins, extensions, and features. And much more you don&amp;rsquo;t need.&lt;/p&gt;&#10;&lt;p&gt;But it&amp;rsquo;s open source, free, and, according to its own statements, used by many, many shop operators. Let&amp;rsquo;s take a look at the most well-known e-commerce extension for Wordpress: &lt;a href="https://woocommerce.com/document/build-online-store/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;WooCommerce&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;I immediately found countless guides on setting up a WooCommerce store (for example, from &lt;a href="https://www.greengeeks.com/blog/set-up-woocommerce-wordpress-ultimate-guide/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;GeeksForGeeks&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; or &lt;a href="https://themeisle.com/blog/how-to-set-up-woocommerce/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Themeisle&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;), so I&amp;rsquo;ll refrain from providing my own guide.&lt;/p&gt;&#10;&lt;p&gt;After skimming through a few tutorials, I&amp;rsquo;ll summarize:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;A WooCommerce store is easy to set up and configure using a wizard.&lt;/li&gt;&#10;&lt;li&gt;There are many themes available, and easy-to-integrate store designs offer a suitable look for every taste.&lt;/li&gt;&#10;&lt;li&gt;It helps to already have a WordPress site and some basic knowledge of domains, DNS, and hosting.&lt;/li&gt;&#10;&lt;li&gt;Many payment service providers and some shipping providers are preconfigured.&lt;/li&gt;&#10;&lt;li&gt;There are numerous YouTube tutorial videos on the topic, and detailed documentation is also available.&lt;/li&gt;&#10;&lt;li&gt;Countless plugins allow you to optimize your store for even very specialized areas and purposes.&lt;/li&gt;&#10;&lt;li&gt;However, this variety of options can be offset by the fact that choosing the &amp;ldquo;best solution&amp;rdquo; can be difficult, and optimizing the site for speed and search engines can become complex.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;All in all, WooCommerce strikes me as a balanced mix of &amp;ldquo;I want to quickly put together something useful&amp;rdquo; and &amp;ldquo;I want to be able to configure and modify everything.&amp;rdquo;&lt;/p&gt;&#10;&lt;h3 id="snipcart"&gt;Snipcart&lt;/h3&gt;&#10;&lt;p&gt;I would consider Snipcart a &amp;ldquo;shopping plugin&amp;rdquo; for any website. Even static websites that can essentially only display a product catalog become a fully-fledged eCommerce solution with Snipcart. The core element here is the shopping cart, which is used to process the purchase.&lt;/p&gt;&#10;&lt;p&gt;The interface is the website&amp;rsquo;s &lt;code&gt;HTML&lt;/code&gt; markup. Snipcart is integrated as a &lt;code&gt;JavaScript&lt;/code&gt; blob and referenced via &lt;code&gt;HTML&lt;/code&gt;. The design and appearance are handled via cascading style sheets. In my case, I could use the existing &amp;ldquo;tech stack&amp;rdquo; consisting of a static Jekyll site with a CD pipeline in Gitea on a Caddyserver instance for the shop as well, and with minimal adjustments, conjure a seamless look for the blog and shop. Sounds great, right?&lt;/p&gt;&#10;&lt;p&gt;Snipcart is developed by a Canadian company in Quebec. It describes itself as a &lt;a href="https://snipcart.com/ecommerce-inventory-management" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&amp;ldquo;Shopping Cart Platform&amp;rdquo;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. An account is required for installation, and you must enter your shopping domain in the Snipcart web frontend, similar to Google Search Console or Google Analytics, to receive a so-called API key. Only with this key will the JavaScript blob integrate correctly.&lt;/p&gt;&#10;&lt;p&gt;And this brings us to the first major problem: Snipcart restricts my freedom.&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;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;preconnect&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;&amp;lt;https://app.snipcart.com&amp;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;preconnect&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;&amp;lt;https://cdn.snipcart.com&amp;gt;&amp;#34;&lt;/span&gt; /&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This shows me that Snipcart would like to keep the data on its server. I have to use their content delivery network to handle the order process and integrate it with mine. So, it&amp;rsquo;s by no means the case that all connections remain with me and I &amp;ldquo;buy&amp;rdquo; the JS code from Snipcart and run it in the customer&amp;rsquo;s browser or a database solution behind it on my server.&lt;/p&gt;&#10;&lt;p&gt;So, if full autonomy and control over your own site isn&amp;rsquo;t a priority, Snipcart can be a very elegant e-commerce solution. This is especially worthwhile for existing sites. However, for me, it&amp;rsquo;s out of the question.&lt;/p&gt;&#10;&lt;h3 id="opencart"&gt;OpenCart&lt;/h3&gt;&#10;&lt;p&gt;&lt;a href="https://www.opencart.com/index.php?route=common/home" target="_blank" rel="noopener noreferrer" class="external-link"&gt;OpenCart&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; offers a PHP-based shop module free of charge and open source. It can be booked as &amp;ldquo;Software as a Service&amp;rdquo; through third-party providers or downloaded via &lt;a href="https://github.com/opencart/opencart/blob/master/INSTALL.md" 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; and deployed on your own server.&lt;/p&gt;&#10;&lt;p&gt;Installation and administration are handled via web interface and, in my opinion, are straightforward. You can also find a few images on DockerHub that you can get started with right away.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-05-15-opencart.avif" alt="Image: OpenCart demo, category overview"&gt;&lt;/figure&gt;&#10;&lt;p&gt;OpenCart is fully customizable to your needs. Templated themes can also be integrated. Out of curiosity, I visited a few websites that use OpenCart: In my opinion, the shop system is well-structured and easy to use. However, the loading speed of the sample pages isn&amp;rsquo;t always super fast.&lt;/p&gt;&#10;&lt;p&gt;Overall, I consider OpenCart, with its customizable, lightweight shop solution in PHP, to be a good solution - also thanks to its still-active community (13+ years!).&lt;/p&gt;&#10;&lt;h3 id="django"&gt;Django&lt;/h3&gt;&#10;&lt;p&gt;&lt;a href="https://www.djangoproject.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Django&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; is a web framework for Python. It offers many features, allows extensions by simply including Python modules, and allows even beginners to quickly create working solutions. Although there are numerous libraries for websites, shops &lt;a href="https://django-shop.readthedocs.io/en/latest/tutorial/intro.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;such as django-SHOP&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, database integrations, integration of payment systems, etc., everything still has to be put together and configured by the developer.&lt;/p&gt;&#10;&lt;p&gt;While this gives you full control and a solution that&amp;rsquo;s perfectly tailored to your needs, it also has a significant drawback: learning time. Database security must be ensured by the developer, and user input validation must be programmed or at least correctly configured. Customer master data is stored in plain text by default. Access and deletion concepts must be created and implemented by the developer, or time should be allowed to identify and integrate suitable modules.&lt;/p&gt;&#10;&lt;p&gt;If frameworks are to be used, it is recommended to check for active community maintenance beforehand. For example, it looks to me as if there hasn&amp;rsquo;t been much activity on the Github repo for &lt;a href="https://github.com/awesto/django-shop" target="_blank" rel="noopener noreferrer" class="external-link"&gt;django-SHOP&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; since 2021. A quick search, however, reveals &lt;a href="https://github.com/topics/django-ecommerce" target="_blank" rel="noopener noreferrer" class="external-link"&gt;a lot of alternatives&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; - now it&amp;rsquo;s time to consider something suitable or reinvent the wheel yourself 😃&lt;/p&gt;&#10;&lt;h2 id="the-alternative-for-beginners-like-me-no-shop-system"&gt;The alternative for beginners like me: no shop system&lt;/h2&gt;&#10;&lt;p&gt;What is meant by &amp;ldquo;no shop system&amp;rdquo;? A form -based approach. Here the user looks at a static website that contains catalog and product pages. In addition, a few elements are shown: E.g. a drop-down menu for variant and a field for the number of pieces. Next to it a simple button. If this is clicked, a form opens that automatically transmits the clicked content and asks about the user&amp;rsquo;s contact details.&lt;/p&gt;&#10;&lt;p&gt;If everything is filled in properly, the form creates a PGP-encrypted email to me when sending. I can then view the order analogously to manually created order emails, send an order confirmation and request payment.&lt;/p&gt;&#10;&lt;p&gt;With a few products, a shop equivalent can be implemented supporting a few customers without much effort. Of course not as comfortable as with an eCommerce solution. But maybe that&amp;rsquo;s enough for the beginning.&lt;/p&gt;&#10;&lt;h2 id="in-the-next-article"&gt;In the next article&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://blog.schallbert.de/en/ecommerce-selection/"&gt;Selection and Implementation&lt;/a&gt;&lt;/p&gt;&#10;</description></item><item><title>Creating an own Webshop - Part 1</title><link>https://blog.schallbert.de/en/ecommerce-requirements/</link><pubDate>Tue, 15 Apr 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/ecommerce-requirements/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-04-15-ecommerce-pt1-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: text saying &amp;#39;&amp;#39;E-commerce Pt1: Requirements of a technophiliac&amp;#39;&amp;#39;"&#10; title="Creating an own Webshop - Part 1" /&gt;&#10;&lt;p&gt;This is part one of a series of articles about building my own webshop. I&amp;rsquo;m listing my requirements and wishes for my e-commerce solution. This way I narrow it down to a few suitable software solutions that I could later use for my webshop.&lt;/p&gt;&#10;&lt;h2 id="what-my-webshop-needs-to-be-able-to-do"&gt;What my webshop needs to be able to do&lt;/h2&gt;&#10;&lt;p&gt;For me as owner, the webshop should operate as lean, transparent, and automated as possible. I want to spend little time on operation, maintenance, and support. Payment processing, order status management, customer registration, sending customer information, and a large number of steps in the shipping process, such as postage and entering shipping confirmations, should be automated or at least prepared for automation.&lt;/p&gt;&#10;&lt;p&gt;But the webshop must also be structured in a way that is understandable for customers: simple and with just a few clicks from the homepage to checkout, easy to use, similar to other common webshops, with straightforward payment processing. As little data as possible should be collected from the customer.&lt;/p&gt;&#10;&lt;p&gt;My webshop only needs to display a few products. Therefore, navigation can be kept very simple. Due to the small number of product categories, a tree structure may be completely avoidable.&lt;/p&gt;&#10;&lt;p&gt;Let&amp;rsquo;s take a detailed look at what features the web shop ultimately needs to provide.&lt;/p&gt;&#10;&lt;h3 id="functions-for-admins"&gt;Functions for Admins&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Create a product&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Title&lt;/li&gt;&#10;&lt;li&gt;Tags&lt;/li&gt;&#10;&lt;li&gt;Short description&lt;/li&gt;&#10;&lt;li&gt;Description&lt;/li&gt;&#10;&lt;li&gt;Images&lt;/li&gt;&#10;&lt;li&gt;Videos&lt;/li&gt;&#10;&lt;li&gt;Price&lt;/li&gt;&#10;&lt;li&gt;Profile / Data sheet / Instructions&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;ol start="2"&gt;&#10;&lt;li&gt;Edit a product&lt;/li&gt;&#10;&lt;li&gt;Remove a product&lt;/li&gt;&#10;&lt;li&gt;Manage orders&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Confirm a step&lt;/li&gt;&#10;&lt;li&gt;Restart a step&lt;/li&gt;&#10;&lt;li&gt;Cancel a process&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;ol start="5"&gt;&#10;&lt;li&gt;Manage customers&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Create (sandbox)&lt;/li&gt;&#10;&lt;li&gt;Delete (data protection)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="functions-for-buyers"&gt;Functions for Buyers&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Browse the webshop&lt;/li&gt;&#10;&lt;li&gt;Configure product(s) or select product variants (e.g., color, quality)&lt;/li&gt;&#10;&lt;li&gt;Add product(s) to a shopping cart&lt;/li&gt;&#10;&lt;li&gt;Manage shopping cart&lt;/li&gt;&#10;&lt;li&gt;Register (optional, only for customer data storage and order management)&lt;/li&gt;&#10;&lt;li&gt;Select shipping&lt;/li&gt;&#10;&lt;li&gt;Make a payment&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="automatic-functions"&gt;Automatic functions&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Payment registration&lt;/li&gt;&#10;&lt;li&gt;Invoice creation and dispatch&lt;/li&gt;&#10;&lt;li&gt;Inventory management&lt;/li&gt;&#10;&lt;li&gt;Shipping notification&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="non-functional-requirements"&gt;Non-functional Requirements&lt;/h2&gt;&#10;&lt;p&gt;Here I list everything the web shop should be able to do &amp;ldquo;on the side.&amp;rdquo; Things that are not directly required for order processing or visible to customers.&lt;/p&gt;&#10;&lt;h3 id="usability-on-the-operator-side"&gt;Usability on the Operator Side&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;The shop should have as few dependencies on third-party software as possible and only integrate those modules that are absolutely necessary. Examples:&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Payment processing&lt;/li&gt;&#10;&lt;li&gt;Shopping cart&lt;/li&gt;&#10;&lt;li&gt;Customer account / address data&lt;/li&gt;&#10;&lt;li&gt;Inventory&lt;/li&gt;&#10;&lt;li&gt;Order processing (mailer) for buyers&lt;/li&gt;&#10;&lt;li&gt;Invoicing.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;ol start="3"&gt;&#10;&lt;li&gt;The shop should only contain as many dynamically generated elements as absolutely necessary.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="usability-on-the-buyer-side"&gt;Usability on the Buyer Side&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;The web shop should feel like current, common e-commerce solutions: nimble, clear, simple.&lt;/li&gt;&#10;&lt;li&gt;No obligation to have a customer account.&lt;/li&gt;&#10;&lt;li&gt;The shop should support common payment methods (debit, credit, PayPal, Apple/Google Pay, etc.). Only local (German-based) payment service providers are acceptable.&lt;/li&gt;&#10;&lt;li&gt;Inventory levels and production times should be displayed transparently for the user.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="data-protection"&gt;Data Protection&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;The shop should only collect data necessary for business transactions.&lt;/li&gt;&#10;&lt;li&gt;If technically and legally possible, the shop should be able to operate without a cookie banner.&lt;/li&gt;&#10;&lt;li&gt;Customer accounts should be automatically deleted after two years of non-use (with two weeks&amp;rsquo; notice).&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="look--feel"&gt;Look &amp;amp; Feel&lt;/h3&gt;&#10;&lt;p&gt;Here are some examples of successful, simple shop designs:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://store.caddyserver.com/en-eur/collections/all" target="_blank" rel="noopener noreferrer" class="external-link"&gt;caddyshop&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://dev.to/contentful_blog/how-to-build-an-ecommerce-static-site-with-jekyll-contentful-and-commerce-layer-3c9" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Example ecommerce with Jekyll&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://www.geeksforgeeks.org/e-commerce-website-using-django/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Example ecommerce with Django&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;Requirements derived from this:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;The web shop&amp;rsquo;s structure and color scheme should match my Blog page and provide a consistent look.&lt;/li&gt;&#10;&lt;li&gt;Only static fonts should be used without reloading.&lt;/li&gt;&#10;&lt;li&gt;Product descriptions should be able to be uploaded as Markdown documents (or similar).&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="database-or-file-solution"&gt;Database or file solution?&lt;/h2&gt;&#10;&lt;p&gt;It doesn&amp;rsquo;t always have to be a database. For many applications, only a fraction of the functionality of common database solutions is required, but this significantly increases the complexity of the overall system. For example, if concurrent writing rarely or never occurs and there is no strong data concatenation, a file solution can be simpler, faster, and more efficient &lt;a href="https://engineeringkiosk.dev/podcast/episode/129-simplify-your-stack-files-statt-datenbanken/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Linking a related &amp;ldquo;Engineering Kiosk&amp;rdquo; podcast episode&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;On the other hand, many shop systems come with batteries included, meaning they already have built-in data management that is almost transparent to the programmers.&lt;/p&gt;&#10;&lt;h3 id="file-solution-static-website"&gt;File solution (static website)&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;From the operator&amp;rsquo;s perspective, the web shop should feel like a Git project (everything is a file).&lt;/li&gt;&#10;&lt;li&gt;Changes to the offer, item descriptions, prices, etc. should be able to be pushed to the server via git push – just like with a static website.&lt;/li&gt;&#10;&lt;li&gt;If a pull request is merged into &amp;lsquo;main&amp;rsquo;, the web shop should be rebuilt (CD pipeline) and the changes published to the server.&lt;/li&gt;&#10;&lt;li&gt;Dynamic elements are required for inventory management, shopping carts, customer interaction, and automated functions. Here, it is important to decide which elements are best handled directly in JavaScript and which are better handled via an automation pipeline.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="database-solution-dynamic-website"&gt;Database solution (dynamic website)&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Products are stored in the database.&lt;/li&gt;&#10;&lt;li&gt;The web shop is managed via a web interface.&lt;/li&gt;&#10;&lt;li&gt;Database management should be partially automated, with backups and encryption for customer data provided.&lt;/li&gt;&#10;&lt;li&gt;The database is transparent for the admin: There is no direct interaction; everything is channeled through e.g. a web interface.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="in-the-next-article"&gt;In the next article&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://blog.schallbert.de/en/ecommerce-alternatives/"&gt;Comparing e-commerce solutions&lt;/a&gt; for my webshop&lt;/p&gt;&#10;</description></item><item><title>PeerTube instance migration</title><link>https://blog.schallbert.de/en/fediverse-ups-and-downs/</link><pubDate>Tue, 01 Apr 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/fediverse-ups-and-downs/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/announcements/2025-03-17-makertube.jpg"&#10; class="post-cover"&#10; alt="Image: My MakerTube channel &amp;#39;&amp;#39;CNC corner&amp;#39;&amp;#39;"&#10; title="PeerTube instance migration" /&gt;&#10;&lt;h2 id="brief-definition"&gt;Brief definition&lt;/h2&gt;&#10;&lt;h3 id="fediverse"&gt;Fediverse?&lt;/h3&gt;&#10;&lt;p&gt;&lt;a href="https://fediverse.info/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Fediverse calls something Fediverse&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; when several providers use the same protocol to allow users of these providers to interact freely with each other on digital media. This creates highly interconnected but decentralized services, where free choice of provider is part of the concept. &lt;a href="https://www.theverge.com/24063290/fediverse-explained-activitypub-social-media-open-protocol" target="_blank" rel="noopener noreferrer" class="external-link"&gt;The Verge&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; goes into detail here for your reference.&lt;/p&gt;&#10;&lt;h3 id="peertube"&gt;PeerTube?&lt;/h3&gt;&#10;&lt;p&gt;PeerTube is a decentralized video platform where multiple servers share the load and viewers can also help distribute videos. Roughly speaking, &lt;em&gt;PeerTube&lt;/em&gt; is to &lt;em&gt;YouTube&lt;/em&gt; what &lt;em&gt;Mastodon&lt;/em&gt; is to &lt;em&gt;X&lt;/em&gt;.&lt;/p&gt;&#10;&lt;h2 id="the-reason"&gt;The Reason&lt;/h2&gt;&#10;&lt;p&gt;This article begins with an email.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;You&amp;rsquo;re receiving this non-automated email because you&amp;rsquo;re one of the handful of people who has a video hosted on this PeerTube instance. I have decided to shut down this service [&amp;hellip;]&amp;rdquo; - Tom, &lt;em&gt;scitech.video&lt;/em&gt; administrator&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;He was really sorry, but continuing to operate the instance would cost too much time, maintenance, and money. Furthermore, the platform is currently underused, so the effort seems disproportionate.&lt;/p&gt;&#10;&lt;p&gt;So far, so understandable.&lt;/p&gt;&#10;&lt;h2 id="toms-story-scitechvideo"&gt;Tom&amp;rsquo;s Story: &lt;em&gt;scitech.video&lt;/em&gt;&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-04-01-scitechvideo-stats.avif" alt="Image: scitech.video statistics as of Mid-March 2025: ~16k views total"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Since I had uploaded and linked all my videos there, I was very interested in the background. So I asked for more information. My questions were answered comprehensively and promptly.&lt;/p&gt;&#10;&lt;h3 id="the-beginnings"&gt;The Beginnings&lt;/h3&gt;&#10;&lt;p&gt;Tom set up his PeerTube instance in 2019. Originally to share his own content. Shortly thereafter, he opened the hosting service to others, albeit with some unease.&lt;/p&gt;&#10;&lt;p&gt;From an admin perspective, such a video platform seems like the &amp;ldquo;Wild West&amp;rdquo;. Ensuring healthy growth for its offerings has proven quite difficult, especially because of the many bots and advertisers involved.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Growing the service turned out to be challenging. Every time I opened registrations spambots started creating accounts easily and uploading videos with advertisements in the video descriptions [&amp;hellip;]&amp;rdquo; - Tom, &lt;em&gt;scitech.video&lt;/em&gt; administrator, via E-Mail&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h3 id="lots-of-bots-and-unwanted-users-around"&gt;Lots of bots and unwanted users around&lt;/h3&gt;&#10;&lt;p&gt;While he was always able to prevent these unwanted spammers, scammers, and advertisers from contaminating &lt;em&gt;scitech.video&lt;/em&gt;, this still meant constant work and manual monitoring of the automatic rejection and moderation tools.&lt;/p&gt;&#10;&lt;p&gt;At this point, I&amp;rsquo;d like to agree with Tom. I have an average of about &lt;em&gt;340&lt;/em&gt; failed connections from external IP addresses to my server per day, which I then block. On the ssh port alone. And my server is neither particularly large nor well-known, nor can it be considered a worthwhile target. Not to mention the countless attempts to access my Gitea user interface, crawl the website for hidden links, and much more.&lt;/p&gt;&#10;&lt;p&gt;For operators of a portal that also grants external users write access (videos, descriptions, tags, comments), the problem is even more serious. Unlike my site, participation is part of the concept here.&lt;/p&gt;&#10;&lt;h3 id="copyright"&gt;Copyright?&lt;/h3&gt;&#10;&lt;p&gt;Exactly. From the platform operator&amp;rsquo;s perspective, every video is fundamentally subject to copyright or related rights. But is the user who wants to publish this video also the author? Or has this person acquired usage rights?&lt;/p&gt;&#10;&lt;p&gt;Depending on the country and the associated legal situation, administrators must address this issue very carefully. High penalties can be imposed on both administrators and users if a copyright infringement occurs.&lt;/p&gt;&#10;&lt;p&gt;The former, at least in Germany, can be resolved according to a &lt;a href="https://datenbank.nwb.de/Dokument/1055605/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Federal Court of Justice ruling of October 23, 2024&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, as follows: &amp;ldquo;[&amp;hellip;] after a clear indication of a violation of law, the content posted there must be checked for similar violations within the scope of what is technically and economically reasonable, and infringing content must be blocked or deleted.&amp;rdquo;&lt;/p&gt;&#10;&lt;p&gt;However, due to the authorities&amp;rsquo; strong focus on specific subject areas, violations are usually quickly noticed – because users can also &amp;ldquo;report&amp;rdquo; posts.&lt;/p&gt;&#10;&lt;h3 id="the-legal-situation-for-social-networks"&gt;The legal situation for social networks&lt;/h3&gt;&#10;&lt;p&gt;Some countries are currently preparing laws to protect children and young people from using social networks and the associated dangers. For reference, here is a link to the Federal Agency for Youth and Media Protection. Here, too, the administrators are required to implement these rules – but how?&lt;/p&gt;&#10;&lt;p&gt;At least, this is what Tom is asking himself, given the increasingly diverse legal interpretations of different countries.&lt;/p&gt;&#10;&lt;h3 id="updates-and-maintenance"&gt;Updates and Maintenance&lt;/h3&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;PeerTube upgrades were a bit of a drag because everything was tightly coupled with their specific nginx configuration, with breaking changes that had to be adjusted every couple of minor versions.&amp;rdquo; - Tom, &lt;em&gt;scitech.video&lt;/em&gt; administrator, via email&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;The work isn&amp;rsquo;t particularly difficult to perform, but it does take a lot of time. As with other software, it can happen that a PeerTube server won&amp;rsquo;t boot after an operating system update, for example, or that incompatibilities arise between different software versions or modules.&lt;/p&gt;&#10;&lt;p&gt;For performance reasons, testing proves difficult, so work is often carried out directly in &amp;ldquo;Prod.&amp;rdquo; In the event of an error, live operation suffers.&lt;/p&gt;&#10;&lt;p&gt;The configuration is relatively complex and has to be adapted to the hardware. Major PeerTube updates also require changes here.&lt;/p&gt;&#10;&lt;h3 id="conclusion"&gt;Conclusion&lt;/h3&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;I&amp;rsquo;m also disillusioned about the fediverse generally as a communication medium. A constant battle against spam/LLMs, so much anger, so much focus on follows, likes, and boosts.&amp;rdquo; - Tom, &lt;em&gt;scitech.video&lt;/em&gt; administrator, via email&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Tom is frustrated that the attention economy is taking hold here as well. Although conceived as ad-free and rolled out as a small grassroots movement, instances created out of idealism are quickly misused for advertising and self-promotion. The costs also amount to several hundred dollars a year, which further worsens the cost-benefit ratio.&lt;/p&gt;&#10;&lt;p&gt;One solution here, according to Tom, could be &amp;ldquo;a return to Web 1.0 or closed communities,&amp;rdquo; as already exists on &lt;em&gt;Meatspace&lt;/em&gt;, &lt;em&gt;Matrix&lt;/em&gt;, &lt;em&gt;Discord&lt;/em&gt;, etc.&lt;/p&gt;&#10;&lt;p&gt;All the other factors also make it difficult to continue operating his instance: The user base never grew particularly large, and there are only a few people who still upload their content there. As a father, he now has much less time available and must prioritize projects. Thus, he is now ending his six-year journey as a provider on the Fediverse.&lt;/p&gt;&#10;&lt;p&gt;I wish Tom continued strength, idealism, and enthusiasm for the future.&lt;/p&gt;&#10;&lt;h2 id="jans-story-makertubenet"&gt;Jan&amp;rsquo;s story: &lt;em&gt;makertube.net&lt;/em&gt;&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-04-01-makertube-stats.avif" alt="Image: MakerTube statistics as of Mid March-2025: ~120k views total"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Since I now needed to look for a new hosting platform for my videos, I tried searching for &amp;ldquo;video-maker&amp;rdquo; on the &lt;a href="https://joinpeertube.org/instances?profile=video-maker" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PeerTube website&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Since my content has a clear tech focus and ranges from research and product development to pre-production, it wasn&amp;rsquo;t difficult to quickly find suitable candidates.&lt;/p&gt;&#10;&lt;p&gt;That&amp;rsquo;s how I came to my channel on &lt;em&gt;MakerTube&lt;/em&gt;.&lt;/p&gt;&#10;&lt;p&gt;After successfully registering, I wrote an email to the admins with similar questions to the ones I had already asked Tom.&lt;/p&gt;&#10;&lt;h3 id="motivation---why-makertube"&gt;Motivation - why MakerTube?&lt;/h3&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;I have a small YouTube channel, and the constant advertising and focus on the big channels, clickbait, etc., and how people are forced to adapt to the content machine, was getting on my nerves. [&amp;hellip;]&amp;rdquo; - Jan, &lt;em&gt;MakerTube&lt;/em&gt; administrator, via email&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Jan was already active on the Fediverse and also works professionally with web services that need to scale rapidly. Therefore, it seemed logical to him to set up his own instance – but of course, with the really interesting topics of this world. It&amp;rsquo;s not intended as a competitor to &lt;em&gt;YouTube&lt;/em&gt; and therefore focuses on makers, musicians, artists, etc.&lt;/p&gt;&#10;&lt;h3 id="is-it-easy-to-get-started"&gt;Is it easy to get started?&lt;/h3&gt;&#10;&lt;p&gt;Jan mentions it takes a lot of effort to set up such an instance and keep it performing well. &lt;a href="https://framasoft.org/en/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Framasoft&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, who develop the PeerTube software, have done a fantastic job and support the hosting community wherever they can.&lt;/p&gt;&#10;&lt;p&gt;I can personally confirm that PeerTube feels completely different in its newer versions than it did a few years ago, and with practical features like automatically generated subtitles, it&amp;rsquo;s getting closer and closer to the functionality of &lt;em&gt;YouTube&lt;/em&gt;.&lt;/p&gt;&#10;&lt;p&gt;Jan says that the Fediverse remains a niche. Therefore, the growth of his instance is rather moderate and very organic. &lt;em&gt;PeerTube&lt;/em&gt; is currently still something for enthusiastic video creators; the content largely goes unnoticed by the general public.&lt;/p&gt;&#10;&lt;h3 id="how-much-time-does-it-take"&gt;How much time does it take?&lt;/h3&gt;&#10;&lt;p&gt;Jan focused on low running costs while maintaining scalability in case the project suddenly becomes large. Now that the system is running smoothly, the effort is a few hours per week for user, content, and support moderation.&lt;/p&gt;&#10;&lt;p&gt;Exceptions: Major updates, upgrades, and technical issues.&lt;/p&gt;&#10;&lt;h3 id="what-are-people-saying"&gt;What are people saying?&lt;/h3&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;There&amp;rsquo;s positive feedback, and people are grateful that I run the site and put in the work, but I&amp;rsquo;d say not very much.&amp;rdquo; - Jan, &lt;em&gt;MakerTube&lt;/em&gt; administrator, via email&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;But of course, there&amp;rsquo;s also occasional outrage that this isn&amp;rsquo;t &lt;em&gt;YouTube&lt;/em&gt; and not everyone can immediately create an account.&lt;/p&gt;&#10;&lt;h3 id="how-do-you-detect-copyright-infringements"&gt;How do you detect copyright infringements?&lt;/h3&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Regarding copyright, there are no direct tools. There&amp;rsquo;s a report function on the videos to point out violations of copyright or instance rules. Users also use this.&amp;rdquo; - Jan, &lt;em&gt;MakerTube&lt;/em&gt; administrator, via email&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Responsibility and liability for small providers like &lt;em&gt;MakerTube&lt;/em&gt; lie primarily with the user. However, if violations are reported, the admins must take action and delete content. On &lt;em&gt;MakerTube&lt;/em&gt;, however, this isn&amp;rsquo;t a problem in everyday life: New user registrations are carefully reviewed, and in cases of doubt, the content is inquired about. Most violations concern background music in videos, so there seems to be less awareness of copyright than with uploads of video copies.&lt;/p&gt;&#10;&lt;h3 id="and-how-is-it-all-financed"&gt;And how is it all financed?&lt;/h3&gt;&#10;&lt;p&gt;&lt;em&gt;MakerTube&lt;/em&gt; is financed through &lt;a href="https://makertube.net/about/instance/support" target="_blank" rel="noopener noreferrer" class="external-link"&gt;donations&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. According to Jan, especially in the beginning, a lot of time was spent explaining this to people and securing sufficient budget for smooth operation. A video platform is resource-intensive: lots of CPU power for transcoding and streaming, a ton of disk space for all the assets, and simultaneously high bandwidth requirements, especially during peak times.&lt;/p&gt;&#10;&lt;h3 id="conclusion-1"&gt;Conclusion&lt;/h3&gt;&#10;&lt;p&gt;I&amp;rsquo;m satisfied with &lt;em&gt;MakerTube&lt;/em&gt; so far: Transcoding is fast, videos run smoothly. Even &lt;em&gt;Heise-Verlag&lt;/em&gt; has now uploaded their first video to the instance. However, it&amp;rsquo;s much more difficult to get to the landing page without a &amp;ldquo;fan base,&amp;rdquo; which was never a problem for me with &lt;em&gt;scitech.video&lt;/em&gt; 😅.&lt;/p&gt;&#10;&lt;p&gt;Let me close with another quote from the site itself:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;MakerTube is here to stay.&amp;rdquo; - &lt;em&gt;MakerTube&lt;/em&gt;, March 2025, about &lt;em&gt;MakerTube&lt;/em&gt;.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Thanks, Jan. Here&amp;rsquo;s to the future!&lt;/p&gt;&#10;</description></item><item><title>Gitea: stop search indexer</title><link>https://blog.schallbert.de/en/gitea-search-indexation/</link><pubDate>Wed, 12 Mar 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/gitea-search-indexation/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-03-12-gitea-indexation-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: How gitea recommends to remove search indexation"&#10; title="Gitea: stop search indexer" /&gt;&#10;&lt;p&gt;After ages, I took a trip to the Google Search Console just for fun. Normally, I&amp;rsquo;m not interested in it because I don&amp;rsquo;t use Google myself and assumed that the search engines would do their job well out of self-interest.&lt;/p&gt;&#10;&lt;p&gt;I was confused when I realized that the majority of my pages didn&amp;rsquo;t even make it through the indexer.&lt;/p&gt;&#10;&lt;h2 id="the-problem"&gt;The problem&lt;/h2&gt;&#10;&lt;p&gt;How are people supposed to find my website? Even if they knew the correct search terms, they would hardly see anything on Google. After all, the console clearly stated that none of my blog posts appeared in the indexer. On my Gitea instance, on the other hand, several thousand pages were listed. A clear disproportion. And why does Gitea generate such a high volume of files?&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-03-12-searchconsole.avif" alt="Image: Google Search console view for git.schallbert.de. Thousands of pages are crawled that a real user will never be interested in."&gt;&lt;/figure&gt;&#10;&lt;p&gt;Google&amp;rsquo;s &lt;a href="https://en.wikipedia.org/wiki/Web_crawler" target="_blank" rel="noopener noreferrer" class="external-link"&gt;crawler&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; (a program that searches the web for content and makes it indexable for search) apparently finds every commit, no matter how small, in public repositories as well as the files behind them. Automation runs and other metadata are also indexed.&lt;/p&gt;&#10;&lt;p&gt;This is of course completely unnecessary and wastes energy in an area that I would much rather have in the presentation of my blog. So a solution is needed.&lt;/p&gt;&#10;&lt;h2 id="possible-solutions"&gt;Possible solutions&lt;/h2&gt;&#10;&lt;p&gt;A few possibilities immediately occurred to me:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Configure the crawler for each repository so that only the top folder level is searched.&lt;/li&gt;&#10;&lt;li&gt;Completely switch off search indexing for &lt;code&gt;git.schallbert.de&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Use Search Console to make corrections until the indexer shows correct assignments on my blog posts.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The most obvious solution for me was to completely switch off the indexer for my Gitea instance. Correction loops only take effect days or weeks later with Google&amp;rsquo;s crawler and turned out too time-consuming. I deemed a separate &lt;code&gt;robots.txt&lt;/code&gt; for each repository too complicated.&lt;/p&gt;&#10;&lt;h2 id="my-implementation"&gt;My implementation&lt;/h2&gt;&#10;&lt;p&gt;I read the &lt;a href="https://docs.gitea.com/administration/search-engines-indexation" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Gitea documentation&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. The suggestion of a &lt;code&gt;robots.txt&lt;/code&gt; made sense to me immediately, as did its contents:&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;User-agent&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;Disallow&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;/&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This tells crawlers from any party &lt;code&gt;*&lt;/code&gt; that nothing should be indexed from the root directory &lt;code&gt;/&lt;/code&gt; onwards.&lt;/p&gt;&#10;&lt;p&gt;Unfortunately, I had no idea how Gitea makes this file available in its instance:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;To make Gitea serve a custom robots.txt (default: empty 404) for top level installations, create a file with path &lt;code&gt;public/robots.txt&lt;/code&gt; in the &lt;a href="https://docs.gitea.com/administration/customizing-gitea" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;code&gt;custom&lt;/code&gt; folder or &lt;code&gt;CustomPath&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h3 id="giteas-configuration-directory"&gt;Gitea&amp;rsquo;s configuration directory&lt;/h3&gt;&#10;&lt;p&gt;So I experimented a bit. I created a &lt;code&gt;public&lt;/code&gt; folder with the corresponding file, then a &lt;code&gt;custom&lt;/code&gt; folder at different levels within the Gitea folder structure. Each time I restarted the container and checked whether the robots file also appeared on the server.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-03-12-whererobots.avif" alt="Image: Where I put robots.txt in Gitea&amp;#39;s folder tree"&gt;&lt;/figure&gt;&#10;Nothing.&lt;/p&gt;&#10;&lt;p&gt;But then I got rid of the folder names and simply put the file in the Gitea configuration directory - the lowest level at which &lt;em&gt;gitea&lt;/em&gt; no longer appears as a folder name. And this was the solution.&lt;/p&gt;&#10;&lt;h3 id="test"&gt;Test&lt;/h3&gt;&#10;&lt;p&gt;After restarting, I was able to successfully display the file in the browser.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--left"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-03-12-robotsolution.avif" alt="Image: Browser successfully loaded my robots.txt for the Gitea instance"&gt;&lt;/figure&gt;&#10;In the meantime, Google is also starting to remove the unwanted pages from the indexer. My blog articles will hopefully also be available in Google search over the next few weeks.&lt;/p&gt;&#10;</description></item><item><title>Halftones on Dibond</title><link>https://blog.schallbert.de/en/halftone-image-on-dibond/</link><pubDate>Tue, 25 Feb 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/halftone-image-on-dibond/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-02-25-bloglogo-stipple-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: halftone image engraved with my CNC"&#10; title="Halftones on Dibond" /&gt;&#10;&lt;p&gt;Lately, I rediscovered Dibond (aluminium sandwich panel) as a material for machining. I use it less and less for environmental reasons - composite materials are not something I consider particularly sustainable or recyclable. Nevertheless, the beautiful surfaces always have a certain attraction for me. Now then - let&amp;rsquo;s get down to business!&lt;/p&gt;&#10;&lt;h2 id="blog-logo-as-a-point-cloud"&gt;Blog logo as a point cloud&lt;/h2&gt;&#10;&lt;p&gt;I was now able to throw an image created by Stipplegen onto the milling machine. To do this I had to solve a problem that was still present in the previous post &lt;a href="https://blog.schallbert.de/en/halftone-cnc/"&gt;Milling grayscale images with the CNC&lt;/a&gt;: In my most frequently used CAM tool, milling paths for point-based halftone images can hardly be created because each of the over 2200 points would have to be individually assigned a milling operation.&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;CADasCAM&lt;/em&gt;, on the other hand, allows you to add entire groups of elements to one and the same milling action. Now I asked myself which milling cutter I should best work with in Dibond, since I only have a very thin covering layer available.&lt;/p&gt;&#10;&lt;h3 id="simulation-radius-cutter"&gt;Simulation radius cutter&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-02-25-r2-ballnose-approximation.jpg" alt="Image: my drawing for V-cut to radius cutter approximation"&gt;&lt;/figure&gt;&#10;I decided on a radius cutter with a half-round head. However, the CAM has no calculation rule for this, so it cannot be selected without further ado. After consulting the manufacturer, the idea came up to simulate the radius cutter for the CAM using a V-cut with a flattened tip, but actually using the ball head on the milling machine.&lt;/p&gt;&#10;&lt;p&gt;In order to get suitable values ​​here, I made a few drawings. The table below contains approximate values ​​for the case where the immersion depth does not exceed &lt;code&gt;0.3mm&lt;/code&gt;. I was now able to carry out the path calculation in the CAM and even adhere to a depth limit of &lt;code&gt;0.22mm&lt;/code&gt;. The flattest areas have an engraving depth of just &lt;code&gt;0.10mm&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;When calculating the path, it is important to note that the retraction heights should be as low as possible. Then the Z-axis of the machines does not have to move up so far for every point. I consider a value of &lt;code&gt;0.5mm&lt;/code&gt; to be appropriate.&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;Radius milling cutter&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Simulation by V-Cut&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;r=3mm&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;140°, 1.0mm flattening&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;r=2mm&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;140°, 0.5mm flattening&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="milling-process"&gt;Milling process&lt;/h3&gt;&#10;&lt;p&gt;The motif size for the engraving is &lt;code&gt;180x180mm&lt;/code&gt; and I mill with the following values: &lt;code&gt;Radius cutter 2s r3 6mm, S20000, F1800&lt;/code&gt;. I pierce the protective film of the Dibond plate, the thickness of which I do not compensate for. I use a small amount of lubricant to improve quality of cut. After just &lt;code&gt;11&lt;/code&gt; minutes, the milling process is finished.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-02-25-r2-bloglogo-stipple.avif" alt="Image: Bloglogo as halftone dot image in Dibond, black"&gt;&lt;/figure&gt;&#10;&lt;h3 id="observations"&gt;Observations&lt;/h3&gt;&#10;&lt;p&gt;The motif is much clearer in this attempt. Unfortunately, I still have a clear problem with the surface flatness. For example, the engraving in the middle area on the right is too deep (built-up edge - see video - that&amp;rsquo;s where the milling machine is at the end), while the engraving depth at the very top right and top center does not seem sufficient.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;The milling machine and milling cutter have no problem with the material at all. The engraving process sounds a bit like working with a box column drill, interrupted by positioning noises from the stepper motors.&lt;/li&gt;&#10;&lt;li&gt;The type of aluminum &lt;em&gt;AlMg1&lt;/em&gt; used for Dibond is soft and chewy. It is not well suitable for machining.&lt;/li&gt;&#10;&lt;li&gt;Milling cutters tend to create &lt;a href="https://en.wikipedia.org/wiki/Built_up_edge" target="_blank" rel="noopener noreferrer" class="external-link"&gt;built-up edges&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, which increase cutter radius and worsen the cutting quality.&lt;/li&gt;&#10;&lt;li&gt;The protective film prevents tears and chip edges.&lt;/li&gt;&#10;&lt;li&gt;Applying a little lubricant is a good idea.&lt;/li&gt;&#10;&lt;li&gt;Even on a vacuum table, every speck of dust under the workpiece ruins the milling work. A few hundredths of a millimeter difference in height has a major impact on the point size and leads to distortions.&lt;/li&gt;&#10;&lt;li&gt;The machine bed must be absolutely flat or the machine must have high-quality, active height compensation; e.g. by means of prior surface measurement using an &lt;a href="https://blog.schallbert.de/en/touching-off/"&gt;edge finder&lt;/a&gt;. My control software even supports the procedure under the name &lt;em&gt;ZheightComp&lt;/em&gt;.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="conclusion"&gt;Conclusion&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;I really like Dibond as a material, but it doesn&amp;rsquo;t seem to return my affection.&lt;/li&gt;&#10;&lt;li&gt;With my technical means, I can&amp;rsquo;t produce engravings that are repeatable and of good quality.&lt;/li&gt;&#10;&lt;li&gt;Maybe I should use plastic or wood instead. Or use a laser&amp;hellip;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Finally, a short time-lapse video of the milling process.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;iframe&#10; src="https://makertube.net/videos/embed/qTd6Ad2QStCp5AiWmrHZem"&#10; title="Halftone on CNC - Part2"&#10; loading="lazy"&#10; allow="autoplay; fullscreen; picture-in-picture"&#10; allowfullscreen&#10; style="width: 100%; height: auto; min-height: 315px; aspect-ratio: 16/9;"&#10; &gt;&lt;/iframe&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Halftone on CNC - Part2&lt;/span&gt;&lt;a href="https://makertube.net/videos/embed/qTd6Ad2QStCp5AiWmrHZem" class="attr-link" target="_blank" rel="noopener noreferrer"&gt;↗&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="saturn-as-ascii-art"&gt;Saturn as ASCII art&lt;/h2&gt;&#10;&lt;p&gt;Halftone images can of course also be created with patterns instead of lines, waves and dots. As an example, I prepared the image of the planet Saturn from &lt;a href="https://asciiart.website/index.php?art=nature/astronomy" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Christopher Johnson&amp;rsquo;s ASCII art&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; for the CNC machine.&lt;/p&gt;&#10;&lt;h3 id="cadcam"&gt;CAD/CAM&lt;/h3&gt;&#10;&lt;p&gt;To get the aspect ratio correct, a font is required in which each character has the same type width. I choose the TrueType font &lt;em&gt;Courier New&lt;/em&gt; and adjust the font size and line spacing so that the motif fits on my material and the engraving does not perforate the cover layer.&lt;/p&gt;&#10;&lt;p&gt;This forces me to use a &lt;code&gt;1/8&amp;quot; engraving bit with a 36° tip angle and 0.1mm flattening&lt;/code&gt;. I would have preferred a radius cutter here: In soft Dibond, acute-angled endmills cause material to bulge and smear. This leads to unclean lines and a rough surface. However, since the workpiece is very small, the font would be much too wide.&lt;/p&gt;&#10;&lt;p&gt;The path calculation shows that I have to choose bold letters on the workpiece, which is only &lt;code&gt;80x80mm&lt;/code&gt; in size, in order to even reach my minimum depth of &lt;code&gt;0.08mm&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="production"&gt;Production&lt;/h3&gt;&#10;&lt;p&gt;I made the image of Saturn twice: on &lt;code&gt;80x80mm&lt;/code&gt; to test the maximum resolution in the material and on &lt;code&gt;180x180mm&lt;/code&gt; (image below) to check contrast and wider letters.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-02-25-halftone-saturn.avif" alt="Image: Halftone image of Saturn from my CNC machine. Image rights: asciiart.website"&gt;&lt;/figure&gt;&#10;&lt;p&gt;I am happy with the result. The contrast is high and the silver-white surface of the aluminum (oxide) looks really classy on the matt black paint.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;iframe&#10; src="https://makertube.net/videos/embed/wxcEMorVuY3hVZkseFjXyU"&#10; title="Halftone on CNC - Part3"&#10; loading="lazy"&#10; allow="autoplay; fullscreen; picture-in-picture"&#10; allowfullscreen&#10; style="width: 100%; height: auto; min-height: 315px; aspect-ratio: 16/9;"&#10; &gt;&lt;/iframe&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Halftone on CNC - Part3&lt;/span&gt;&lt;a href="https://makertube.net/videos/embed/wxcEMorVuY3hVZkseFjXyU" class="attr-link" target="_blank" rel="noopener noreferrer"&gt;↗&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="qr-codes"&gt;QR codes&lt;/h2&gt;&#10;&lt;p&gt;While I&amp;rsquo;m already working with Dibond: I&amp;rsquo;ll try cutting a few QR codes with a radius bit.&#10;This time I&amp;rsquo;m using different machines.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-02-25-qr-dibond.avif" alt="Image: QR-codengrave tests in dibond with a radius cutter"&gt;&lt;/figure&gt;&#10;&lt;h3 id="on-the-hobbyline"&gt;On the Hobbyline&lt;/h3&gt;&#10;&lt;p&gt;I engrave in black on the Hobbyline provided to me in overclocked engraving mode. I fix the workpiece with a few dots of double-sided tape. I mill dry. Observations:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;The engraving depth seems to increase with increasing line length (built-up cutting edge, milling cutter smears into the material)&lt;/li&gt;&#10;&lt;li&gt;In some cases the paint surface at the milling edge bulges upwards&lt;/li&gt;&#10;&lt;li&gt;Very dirty milling channels, strong direction dependency&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="on-the-basicline"&gt;On the Basicline&lt;/h3&gt;&#10;&lt;p&gt;Here I naturally use my vacuum table and benefit from the much more stable structure. I use a small amount of lubricant. I was curious to see whether the white material still allows sufficient contrast. Observations:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;My &lt;a href="https://blog.schallbert.de/en/projects/qr-codengrave/"&gt;QR-codeengrave&lt;/a&gt; software must be operated correctly. Otherwise, zero points and size ratios shift, as here. Result: My cell phone does not recognize the QR code 😐&lt;/li&gt;&#10;&lt;li&gt;The much better hold down prevents major differences in milling channel width.&lt;/li&gt;&#10;&lt;li&gt;Nevertheless, you can see the machine&amp;rsquo;s exit points and sometimes also dragged chips.&lt;/li&gt;&#10;&lt;li&gt;The milling quality is still not satisfactory.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="conclusion-1"&gt;Conclusion&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-11-01_smallparts-thumb.jpg" alt="Image: single-flute cutter for PMMA/Acrylics."&gt;&lt;/figure&gt;&#10;In the future, I will have to treat Dibond in a similar way to acrylic glass: use very sharp endmills that cut the material properly. Ideally, single-flute cutters with a hawk&amp;rsquo;s beak and corner bevel (soft edges), center-cutting, and a decent twist. E.g. &lt;a href="https://shop.vhf.de/articleGroups/Einzahnfraeser-mit-Stirnradius-W_F_ES_SCR.htm" target="_blank" rel="noopener noreferrer" class="external-link"&gt;this one (link to vhf)&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. This - I believe - prevents material displacement into the even softer core material and also moves chips upwards in the chip channel so that they cannot stick or get under the milling cutter a second time.&lt;/p&gt;&#10;&lt;p&gt;I should also work with rapid feed rates. Then the material does not heat up too much. Using a little lubricant and applying protective film contributes to better milling results. Further processing instructions can be found, for example, &lt;a href="https://www.cnc-aus-holz.at/index.php?thread/2397-aluverbundplatte-dibond-gravieren/&amp;amp;postID=27536#post27536" target="_blank" rel="noopener noreferrer" class="external-link"&gt;in the CNC-aus-Holz forum&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. It may be worth browsing.&lt;/p&gt;&#10;</description></item><item><title>CNC-made greyscale images</title><link>https://blog.schallbert.de/en/halftone-cnc/</link><pubDate>Mon, 27 Jan 2025</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/halftone-cnc/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-01-27_bloglogo-thumb.avif"&#10; class="post-cover"&#10; alt="Image: TSP-paths of my blog&amp;#39;&amp;#39;s logo"&#10; title="CNC-made greyscale images" /&gt;&#10;&lt;p&gt;Up until now, I have always created images as engravings in solid colors. This allows texts like &lt;a href="https://blog.schallbert.de/en/negative-carving-with-estlcam/"&gt;this one&lt;/a&gt;, but also &lt;a href="https://blog.schallbert.de/en/engrave-multicolor/"&gt;drawings&lt;/a&gt; and &lt;a href="https://blog.schallbert.de/en/makerfaire-ruhr/#preparation"&gt;real images&lt;/a&gt; to be produced with high contrast and attractively.&lt;/p&gt;&#10;&lt;h2 id="the-goal"&gt;The goal&lt;/h2&gt;&#10;&lt;p&gt;With solid color representation, many details cannot be reproduced. But now I would like to bring a photo into the material. Ideally, this should happen quickly, without manual rework and with high image quality.&lt;/p&gt;&#10;&lt;p&gt;My CAM tool provides a &amp;ldquo;halftone image&amp;rdquo; function for this, which I will experiment with below. Here, too, it is unlikely to get good results of the machine straight away. In addition, the creation of such images is very time-consuming.&lt;/p&gt;&#10;&lt;p&gt;Therefore, at the end of the article I will give a few tips and tricks that can help you achieve good results more quickly.&lt;/p&gt;&#10;&lt;h2 id="a-little-color-knowledge-rasterization"&gt;A little color knowledge: Rasterization&lt;/h2&gt;&#10;&lt;p&gt;The problem of wanting to represent an entire color space with only a few available colors is hundreds of years old. While color gradients can be excellently drawn by mixing colors in the analog world and also on the screen using different luminance values ​​of the three color channels in pixels, not every application has a large number of colors available.&lt;/p&gt;&#10;&lt;p&gt;Printers, for example&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;. They usually only have the three basic colors &lt;code&gt;cyan (C)&lt;/code&gt; - a shade of turquoise, &lt;code&gt;magenta (M)&lt;/code&gt; - a shade of pink, &lt;code&gt;yellow (Y)&lt;/code&gt; - yellow and &lt;code&gt;key (K)&lt;/code&gt; - usually black - at their disposal. From these they then have to create the illusion of all possible colors, because they cannot mix the pigments due to their high opacity. Otherwise the last color applied would dominate or the color would simply run plain. This results in the need to use dot grid or other rasterization processes.&lt;/p&gt;&#10;&lt;h3 id="halftones-halftone-image"&gt;Halftones (halftone image)&lt;/h3&gt;&#10;&lt;p&gt;One of these raster processes is called &lt;em&gt;halftone&lt;/em&gt;. This involves using colored dots or patterns of different line thicknesses to simulate a &lt;a href="https://en.wikipedia.org/wiki/Halftone" target="_blank" rel="noopener noreferrer" class="external-link"&gt;color tone&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, which the eye can no longer resolve from a certain distance and therefore &amp;ldquo;averages&amp;rdquo; together with the surrounding free space and possibly other colored dots to form a tone value.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-01-27_bloglogo_9600dots.avif" alt="Image: 9600 dots stipple image of my blog&amp;#39;s logo"&gt;&lt;/figure&gt;&#10;&lt;p&gt;This process is suitable for &lt;a href="https://hackaday.com/2011/07/28/creating-halftone-pictures-with-a-cnc-machine/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;production using a CNC milling machine&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, provided that the surface of the workpiece to be processed has a different color than the core material.&lt;/p&gt;&#10;&lt;p&gt;There are many different production processes for halftones, in which the arrangement of the dots, lines, pattern shape, etc. can sometimes differ greatly. On the CNC, the engraving material used, the required resolution and the desired tool determine the process, since good quality cannot be achieved with every combination.&lt;/p&gt;&#10;&lt;h3 id="dithering"&gt;Dithering&lt;/h3&gt;&#10;&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Dither#Digital_photography_and_image_processing" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Dithering&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; is more commonly used for display on screens. Unlike with the halftone process, the strength or size of colored dots or patterns does not vary, but rather the spatial distribution of differently colored dots that are sprinkled into the base color. This creates the impression of a mixed color.&lt;/p&gt;&#10;&lt;p&gt;Here, too, there are various generation methods: diffusion, noise, patterns, &lt;a href="https://en.wikipedia.org/wiki/Floyd%E2%80%93Steinberg_dithering" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Floyd-Steinberg algorithm&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&amp;hellip;&lt;/p&gt;&#10;&lt;h2 id="material-selection"&gt;Material selection&lt;/h2&gt;&#10;&lt;p&gt;I&amp;rsquo;ve tried out a lot here. The best materials are those with a homogeneous composition and a high contrast between the surface and core color. In addition, the layer thickness of the surface material should not be too large, as it has to be completely pierced and very large individual dots are then quickly required. The surface is ideally matt. Silk, metallic or high gloss surfaces create a viewing angle dependency that affects the recognizability of the motif.&lt;/p&gt;&#10;&lt;p&gt;In addition, the material should be completely flat to the milling surface, as otherwise the dot size varies undesirably.&lt;/p&gt;&#10;&lt;h3 id="suitable-materials-"&gt;Suitable materials 👍&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;Core material&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Coating&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Special feature&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;Medium-density fiberboard&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Primer + paint&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Do not overlap the milling 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;Aluminum composite material&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Paint or film&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Mill to a maximum depth of 0.18mm&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;Acrylic glass (PMMA)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Paint or film&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Use XT (low thickness tolerance)&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;The least effort for me is processing finished painted aluminum composite material. Practically no preparatory or post processing is required here. However, it is important to limit the milling depth to &lt;code&gt;0.18mm&lt;/code&gt;. Otherwise the PU core will be deformed or tear-outs will occur.&lt;/p&gt;&#10;&lt;p&gt;Applying a film can be a good solution if you don&amp;rsquo;t want to spend time applying paint. Unfortunately, I&amp;rsquo;m not particularly skilled at working with film. I&amp;rsquo;m constantly creating air bubbles or inclusions of tiny dust particles. After the milling process, the resulting defects in the height profile are clearly visible in the form of holes in the material that are too large. Plus, when cutting through those bubbles, there&amp;rsquo;s no adhesive force between film and surface material which can lead to film rip-offs.&lt;/p&gt;&#10;&lt;h3 id="unsuitable-materials-"&gt;Unsuitable materials 👎&lt;/h3&gt;&#10;&lt;p&gt;In my opinion, the materials listed here are not well suitable for creating halftone images. I have uploaded a few pictures of unsuccessful attempts here. Often, one reason for failure is that the image resolution is too high for the material.&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;Core material&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Coating&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Base&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;Medium density fiberboard&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Veneer&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Layer thickness too high&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;Medium density fiberboard&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Metal film&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Paper core of the metal film is white&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;Aluminum composite material&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Metal, brushed&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Material &amp;ldquo;mushy&amp;rdquo;, poor resolution&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;Aluminum composite material&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;None, PU core&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Aluminum pressed into core, poor resolution&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;Soft aluminum alloys&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Paint or film&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Aluminum smears, poor resolution&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--left"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-01-27-3guys_perfect.avif" alt="Image: 3 guys as halftone image on metalized MDF"&gt;&lt;/figure&gt;&#10;Here I&amp;rsquo;m experimenting with the pixel density. In the lower area I arranged them too closely so that the coating comes off during milling. On my engraving CNC, such an image on a plate measuring &lt;code&gt;15x10cm&lt;/code&gt; takes about &lt;code&gt;15 minutes&lt;/code&gt; despite overclocking. The material is not well suited for such high resolutions because of the metallic shine, the risk of tearing, hardly achievable surface adhesion, and the high layer thickness.&lt;/p&gt;&#10;&lt;h2 id="halftone-images-requirements-for-the-cnc"&gt;Halftone images: Requirements for the CNC&lt;/h2&gt;&#10;&lt;p&gt;For once, the problem here is not related to holding forces, milling forces or difficulties in holding down. On the contrary, when milling images, low forces are applied and that mostly in Z-direction where the machines have highest stability reserve.&lt;/p&gt;&#10;&lt;p&gt;The complexity of the jobs is the problem. The portal has to cover long distances and constantly brake and accelerate along the way. This puts a lot of strain on the power drivers, stepper motors and bearings of the linear spindles.&lt;/p&gt;&#10;&lt;h3 id="wavesstrokeslines"&gt;Waves/strokes/lines&lt;/h3&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-01-27-anywhereamps_halftonegrid.avif" alt="Image: Halftone wave-grid image of AnywhereAmps1.2 compared to original image"&gt;&lt;/figure&gt;&#10;&lt;p&gt;In the segment process, the machine moves across an image line by line. Brightness information is usually transferred via milling depth, so that particularly dark areas produce wider lines when using an engraving cutter. This process is ideal for milling in foam, MDF and acrylic glass. It can also be carried out quite quickly because the machine remains in contact with the material and positioning processes are only required for line jumps.&lt;/p&gt;&#10;&lt;h3 id="dot-process"&gt;Dot process&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-01-27-bloglogo_stipple.avif" alt="Image: bloglogo made of 2000 Vonoroi stipples"&gt;&lt;/figure&gt;&#10;Here the size of dots is changed so that sometimes larger and sometimes smaller stipples are created in the material.&lt;/p&gt;&#10;&lt;p&gt;Halftone images using a dot process require a relatively long time on the machine. What can be done in half an hour with a hobby laser easily takes two hours with a CNC machine of the same size and resolution (here too there are &lt;a href="https://blog.schallbert.de/en/cnc-tuning-kinematics/"&gt;optimization options&lt;/a&gt;).&lt;/p&gt;&#10;&lt;p&gt;To produce such images, the machine has to move the Z axis extremely often. When milling points, it has to switch between safety height and material engagement once for each &amp;ldquo;pixel&amp;rdquo;.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;Example: If &lt;code&gt;400x400&lt;/code&gt; points are milled (that would be a pretty poorly resolved image at pixel scale), the machine has to set its Z axis in motion &lt;code&gt;32000&lt;/code&gt; times!&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Therefore, the safety height should only be a few tenths above the workpiece surface, e.g. &lt;code&gt;0.5mm&lt;/code&gt;. To protect the heavy Y axis, it makes sense to process the image line by line on the X axis using &lt;em&gt;CAM&lt;/em&gt;. This way, the Y axis is only moved for line jumps.&lt;/p&gt;&#10;&lt;p&gt;A light gantry milling machine with little mass on the Z axis is advantageous here. It should be well lubricated and the axis guides cleaned. At the same time, it doesn&amp;rsquo;t need to be super stiff as not to oscillate due to the many abrupt movements.&lt;/p&gt;&#10;&lt;p&gt;For stepper motors and drivers, these jobs are marathon races. Their temperatures should always be kept under close observation. I recommend not running the machine with halftone images in continuous operation. The axis and portal loads, on the other hand, are negligible: the machine is hardly exposed to any load in the X and Y directions, as it is mostly in contact with the material downwards and only minimally to create conical indentations.&lt;/p&gt;&#10;&lt;h3 id="labyrinthline-segments"&gt;Labyrinth/line segments&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-01-27-bloglogo_tsp.avif" alt="Image: TSP-paths my bloglogo connecting 9600 Vonoroi stipples"&gt;&lt;/figure&gt;&#10;If the entire image is to be covered in just one go, a &amp;ldquo;travelling salesman problem&amp;rdquo; (&lt;a href="https://en.wikipedia.org/wiki/Travelling_salesman_problem" target="_blank" rel="noopener noreferrer" class="external-link"&gt;TSP&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;) must first be solved, such as &lt;a href="https://www.evilmadscientist.com/2012/stipplegen-weighted-voronoi-stippling-and-tsp-paths-in-processing/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;EvilMadScientist&amp;rsquo;s&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; software &lt;em&gt;Stipplegen2&lt;/em&gt; is capable of. In this process, the density of line segments per area is increased if an area is to appear particularly dark. Such images can be completed quickly on the CNC machine, as they only allow the Z axis to dip once.&lt;/p&gt;&#10;&lt;p&gt;The disadvantages of this process are the low achievable resolution (a lot of points are required) and the low contrast, because all lines are the same thickness.&lt;/p&gt;&#10;&lt;h2 id="cnc-tools"&gt;CNC tools&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-01-27-graver.avif" alt="Image: This CNC graver is a good tool for halftone image fabrication"&gt;&lt;/figure&gt;&#10;The creation of halftone images is engraving work. It is therefore recommended to use gravers with a &lt;code&gt;60°&lt;/code&gt; or &lt;code&gt;90°&lt;/code&gt; tip angle and an engraving width of &lt;code&gt;0-0.5mm&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;If the motif is small, an engraving width of 0 may well be chosen. The tip angle should be large if you do not want to or cannot mill deeply or if the &amp;ldquo;light color&amp;rdquo; is the core color of the material. This avoids deep shadows that darken the image.&lt;/p&gt;&#10;&lt;p&gt;If you are machining painted aluminum composite panels, I can recommend half-round cutters with &lt;code&gt;r&amp;lt;2mm&lt;/code&gt; and a very low depth of cut at or below &lt;code&gt;0.18mm&lt;/code&gt;. However, this assumes that the machine bed is face-milled and therefore very evenly aligned.&lt;/p&gt;&#10;&lt;h2 id="manufacturing"&gt;Manufacturing&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-01-27-bloglogo_tsp_machined.avif" alt="Image: TSP-paths on aluminium sandwich"&gt;&lt;/figure&gt;&#10;Image: Attempting to engrave my blog logo as a TSP path with 9600 edges on black painted aluminium composite material. I used a &lt;code&gt;1/8&amp;quot; 36° graver, 0.1mm engrave width&lt;/code&gt;. At the bottom of the image you can see that the material is a little convex and can therefore no longer be reached by the graver.&lt;/p&gt;&#10;&lt;h3 id="image-preparation"&gt;Image preparation&lt;/h3&gt;&#10;&lt;p&gt;The image should have a high resolution and be in greyscale. Ideally it should have high contrast and use the entire greyscale spectrum.&lt;/p&gt;&#10;&lt;p&gt;The image is now loaded either into a CAM tool that supports the creation of halftone images - e.g. from &lt;a href="https://gadgets.vectric.com/v8/halftoner" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Vectric&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, Estlcam - or into an editing tool that can create point clouds or paths, e.g. &lt;a href="https://www.evilmadscientist.com/2012/stipplegen2/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Stipplegen2&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; or Inkscape.&lt;/p&gt;&#10;&lt;p&gt;There I always try out a few different settings depending on the material used, desired cutter and image effect. You can often see there whether the image production would work and you can assess whether the resolution is suitable for the material used.&lt;/p&gt;&#10;&lt;h3 id="how-to-get-good-results"&gt;How to get good results&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Prefer large dimensions: The larger the image is chosen, the less noticeable material inhomogeneities and torn-out spots are.&lt;/li&gt;&#10;&lt;li&gt;Allow a large observation distance: The further the image is positioned from the viewer, the sharper the resolution is perceived. Before it eventually appears too small 😆&lt;/li&gt;&#10;&lt;li&gt;Take the angle of incidence of light into account: If the material is half-matte, metallic or even high-gloss, the image quality depends very much on the viewing angle and the distance to the light source. The shinier the material, the smaller the area in which the image can be clearly seen.&lt;/li&gt;&#10;&lt;li&gt;Use homogeneous, non-smearing materials. I have achieved the best results with acrylic glass.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="cnc-run"&gt;CNC run&lt;/h3&gt;&#10;&lt;p&gt;There is one thing you need to bring with you here: time. Below are a few video clips from my experiments (time lapse).&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;iframe&#10; src="https://makertube.net/videos/embed/n7CnjF5CUn1sidg6mxYhvL"&#10; title="Halftone on CNC - Part1"&#10; loading="lazy"&#10; allow="autoplay; fullscreen; picture-in-picture"&#10; allowfullscreen&#10; style="width: 100%; height: auto; min-height: 315px; aspect-ratio: 16/9;"&#10; &gt;&lt;/iframe&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Halftone on CNC - Part1&lt;/span&gt;&lt;a href="https://makertube.net/videos/embed/n7CnjF5CUn1sidg6mxYhvL" class="attr-link" target="_blank" rel="noopener noreferrer"&gt;↗&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&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;Exceptions prove the rule. Of course there are versions of both screens and printers that get around this problem: They use subpixels with additional colors and can thus cover larger color spaces or - as is common in gravure printing (magazines, packaging, etc.) - use a higher number of colors or color mixtures whose tonal values ​​are closer to the required halftones.&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>Multicolor engravings</title><link>https://blog.schallbert.de/en/engrave-multicolor/</link><pubDate>Mon, 30 Dec 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/engrave-multicolor/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-12-30-multicolorengrave-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: multicolored sign &amp;#39;&amp;#39;Helft Waldbraende verhueten&amp;#39;&amp;#39;"&#10; title="Multicolor engravings" /&gt;&#10;&lt;h2 id="the-project-idea"&gt;The project idea&lt;/h2&gt;&#10;&lt;p&gt;A few weeks ago I was out in the forest. There I saw a beautiful, old sign with the inscription &amp;ldquo;Help prevent wildfires&amp;rdquo;. A drawing style like ink from olden times. I took a photo without further ado.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-12-30_waldbrand-original.jpg" alt="Image: An old *prevent forest fires* sign"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Unfortunately, the sign is quite faded and looks pretty flat as a photo. So I decided to make a three-dimensional copy of the sign with the help of my CNC engraving machine, which would take a little time but not cost much.&lt;/p&gt;&#10;&lt;h2 id="from-jpg-to-svg"&gt;From &lt;code&gt;.jpg&lt;/code&gt; to &lt;code&gt;.svg&lt;/code&gt;&lt;/h2&gt;&#10;&lt;p&gt;Before I can create a job for the CNC, I have to prepare the image and put it into vector form. My CAM program can&amp;rsquo;t do much with pixel graphics. I would like to create the image as a &amp;ldquo;carve&amp;rdquo;, i.e. use a tapered engraving cutter to carve the areas of the image that should later appear in color. For this I also need a color separation, as the image should later have white, red and black areas in the milled version.&lt;/p&gt;&#10;&lt;h3 id="image-preparation"&gt;Image preparation&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-12-30_waldbrand-cleaned.jpg" alt="Image: Preparing the image for trace operation"&gt;&lt;/figure&gt;&#10;For this I use common image editing programs such as &lt;a href="https://www.gimp.org/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;gimp&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and &lt;a href="https://www.getpaint.net/index.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;paint.net&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. I use the &lt;em&gt;curves&lt;/em&gt; tool to increase contrast, straighten the perspective using 3D rotation and then use the &lt;em&gt;crop&lt;/em&gt; tool to get the desired image section. I then erase image defects, dirt or artifacts caused by increasing the contrast. Finally, I add a little sharpness and do a tone separation so that I get pure colors rather than red tones or greyscale.&lt;/p&gt;&#10;&lt;p&gt;Now I export the image as &lt;code&gt;.jpg&lt;/code&gt; and import it into &lt;a href="https://inkscape.org/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Inkscape&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="path-conversion"&gt;Path conversion&lt;/h3&gt;&#10;&lt;p&gt;Here I first scale the image so that it fits on an A4 page. I also create a small &lt;code&gt;1mm x 1mm&lt;/code&gt; square at the bottom left of the image, which will later serve as the zero point for the two color layer images in the CAM, but will not be milled itself.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-12-30_waldbrand-trace.jpg" alt="Image: tracer in Inkscape"&gt;&lt;/figure&gt;&#10;&lt;p&gt;As already described in an earlier &lt;a href="https://blog.schallbert.de/en/path-from-image/#inkscape"&gt;article on path conversion&lt;/a&gt;, I use the trace function and can isolate the red and black areas in separate layers using different brightness thresholds. The &amp;ldquo;red&amp;rdquo; layer initially also contains the black image areas. However, I can subtract layers from each other so that after this action only the red parts of the image remain.&lt;/p&gt;&#10;&lt;p&gt;I then simplify the paths and remove any path artifacts that were created incorrectly. I export the &amp;ldquo;red&amp;rdquo; and &amp;ldquo;black&amp;rdquo; layers as &lt;code&gt;.svg&lt;/code&gt; files.&lt;/p&gt;&#10;&lt;h2 id="path-planning-in-cam"&gt;Path planning in CAM&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--left"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-12-30-feuertrace.jpg" alt="Image: *red* areas to be cut"&gt;&lt;/figure&gt;&#10;In CAM I have to plan two jobs - one for each color. The unprocessed image areas should appear white. The engraving of the first job will be laid out with red color, and the second engraving should later appear deep black.&lt;/p&gt;&#10;&lt;p&gt;So I first import the &amp;ldquo;red&amp;rdquo; SVG file, select a scaling appropriate for DIN A4, set the zero point at the bottom left and then start creating the carve paths.&lt;/p&gt;&#10;&lt;h3 id="job1-red-areas"&gt;Job1: &amp;ldquo;red&amp;rdquo; areas&lt;/h3&gt;&#10;&lt;p&gt;I set a depth limit of &lt;code&gt;2mm&lt;/code&gt; and select a &lt;code&gt;6mm 90°&lt;/code&gt; graver with one cutting edge and &lt;code&gt;0.5mm&lt;/code&gt; flattening at the tip for the engraving. For clearing larger engraved areas, I select a &lt;code&gt;3mm&lt;/code&gt; two-cutter for wood and mill with &lt;code&gt;60%&lt;/code&gt; lateral feed. I set the speed for both milling cutters to &lt;code&gt;24000rpm&lt;/code&gt; and the feed to &lt;code&gt;3000mm/min&lt;/code&gt;.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--left"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-12-30-waldtrace.jpg" alt="Image: *black* areas to be cut"&gt;&lt;/figure&gt;&#10;&lt;h3 id="job2-black-areas"&gt;Job2: &amp;ldquo;black&amp;rdquo; areas&lt;/h3&gt;&#10;&lt;p&gt;For the black areas, I allow a higher depth of &lt;code&gt;2.8mm&lt;/code&gt; for Job2 and save myself the clearing cutter as fewer connected areas have to be removed here. The greater depth should make the black even darker and the motif more three-dimensional.&lt;/p&gt;&#10;&lt;h2 id="fabrication"&gt;Fabrication&lt;/h2&gt;&#10;&lt;p&gt;For the project, I use a black medium density fiber board (MDF). I paint it several times with white acrylic paint until I have a homogeneous and well-covering surface. After everything has dried, I apply a self-adhesive film. Professionals use stencil films (masking films), e.g. from Orafol, but I make do with a much simpler wrapping film for books. Not perfect, as its adhesion is not sufficient for small areas - but good enough for me.&lt;/p&gt;&#10;&lt;h3 id="red-flames"&gt;Red flames&lt;/h3&gt;&#10;&lt;p&gt;Now I run the first job on the milling machine. To do this, I use double-sided tape to attach the workpiece to the milling machine&amp;rsquo;s working area and align it properly so that the zero point can be set easily.&lt;/p&gt;&#10;&lt;p&gt;As soon as the milling job was complete, I put screws in the T-slots of the base plate on the edges of the workpiece. This allows me to position it with repeatable accuracy. Then I remove it from the machine bed, clean it with a brush and apply red spray paint. Unfortunately, this immediately soaks into the MDF (see video below). The flames therefore remain black. I try again, again without success. So I have to go back to the shop and buy a much more viscous paint to apply with a brush.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;If MDF is to be painted, spray paint is not suitable because of its low viscosity: it soaks in and leaves hardly any pigment on the surface.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;After waiting again until the flames have dried, I mount the workpiece on the machine bed again.&lt;/p&gt;&#10;&lt;h3 id="black-firs"&gt;Black firs&lt;/h3&gt;&#10;&lt;p&gt;Now I run the second job through. This is significantly faster despite not using the clearing cutter, as there is no need to change tools.&#10;I then remove the protective film from the workpiece and clean it thoroughly.&lt;/p&gt;&#10;&lt;p&gt;How beautiful the sign turned out!&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-12-30-multicolorengrave.jpg" alt="Image: carving complete: My copy of *Preventing forest fires*."&gt;&lt;/figure&gt;&#10;&lt;h2 id="video-of-the-3d-engraving"&gt;Video of the 3D engraving&lt;/h2&gt;&#10;&lt;p&gt;There is another time-lapse video for this project, provided on my PeerTube channel.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;iframe&#10; src="https://makertube.net/videos/embed/briT2e8qbbY6uWFFmv688n"&#10; title="multicolorEngrave"&#10; loading="lazy"&#10; allow="autoplay; fullscreen; picture-in-picture"&#10; allowfullscreen&#10; style="width: 100%; height: auto; min-height: 315px; aspect-ratio: 16/9;"&#10; &gt;&lt;/iframe&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;multicolorEngrave&lt;/span&gt;&lt;a href="https://makertube.net/videos/embed/briT2e8qbbY6uWFFmv688n" class="attr-link" target="_blank" rel="noopener noreferrer"&gt;↗&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="what-could-be-improved"&gt;What could be improved&lt;/h2&gt;&#10;&lt;p&gt;Looking back, there are often things in my projects that can be done better. When I look at the video, two things stand out to me:&lt;/p&gt;&#10;&lt;h3 id="1-path-quality-determines-milling-quality"&gt;1. Path quality determines milling quality&lt;/h3&gt;&#10;&lt;p&gt;I wanted to get to the result as quickly as possible and therefore did not remove all path artifacts when converting the pixel image to vector graphics. These appear on the finished product as small islands and points that were milled with less depth than the rest. This is best seen in the red areas of the image. In reality, this is hardly noticeable. In the video, however, you can see that the milling machine needs additional time to work out these areas.&lt;/p&gt;&#10;&lt;h3 id="2-cam-strategy-costs-time"&gt;2. CAM strategy costs time&lt;/h3&gt;&#10;&lt;p&gt;My program for milling planning runs milling paths for clearing, outlines and chiseling from top to bottom, line by line. This means that an incredible number of positioning commands are required for non-connected paths. In the worst case each point is approached three times.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;G00&lt;/code&gt; commands are bad here because they take a lot of time and cause the machine to move unnecessarily. This problem can only be solved with a more advanced CAM tool.&lt;/p&gt;&#10;</description></item><item><title>Repo-Lookout: Fix repository leaks</title><link>https://blog.schallbert.de/en/repo-lookout-fix-deploy/</link><pubDate>Fri, 20 Dec 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/repo-lookout-fix-deploy/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-12-20-repolookout-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Repolookout reporting a possible vulnerability on my site"&#10; title="Repo-Lookout: Fix repository leaks" /&gt;&#10;&lt;h2 id="repo-information-publicly-available"&gt;Repo information publicly available&lt;/h2&gt;&#10;&lt;p&gt;One fine day I got an email from &lt;a href="https://www.repo-lookout.org/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Repo Lookout&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. It said that one of my repositories was open to access from the internet. This posed a potential security risk as it might contain secret source files, hidden functions or even passwords.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-12-20_repolookout_warning.jpg" alt="Image: RepoLookout mail notification"&gt;&lt;/figure&gt;&#10;&lt;p&gt;At first I thought this &amp;ldquo;your repo is not secure&amp;rdquo; warning was a phishing attempt.&#10;But by simply entering the links contained therein, it became clear that &lt;em&gt;Repo Lookout&lt;/em&gt; was right and that my &lt;a href="https://lectures.schallbert.de" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Lectures&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; repo was not only public as I had wanted, but that the version control metadata was also openly available on the web server.&lt;/p&gt;&#10;&lt;h3 id="is-that-bad"&gt;Is that bad?&lt;/h3&gt;&#10;&lt;p&gt;Normally, the internal structure and configuration (actions, discussions, wiki, etc.) behind a repository should not be public. Especially not if the repository is set up as &lt;code&gt;private&lt;/code&gt;. But even with public repos, no one should be able to access the structure behind them.&lt;/p&gt;&#10;&lt;p&gt;Hence the mission of Repo-Lookout:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Find source code repositories that have been inadvertently exposed to the public and report them to the domain&amp;rsquo;s technical contact.&amp;rdquo; - Repo Lookout /about (&lt;a href="https://www.crissyfield.de/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Crissy Field GmbH&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;)&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;In this case, not critical, but unexpected and unpleasant.&lt;/p&gt;&#10;&lt;p&gt;Doesn&amp;rsquo;t do me any harm, because my web server only has the files there for retrieval and even if they were tampered with, they would have had no effect on my repository. I also had all secrets stored in specially created files as described &lt;a href="https://blog.schallbert.de/en/server-config-version-control/#secrets-in-docker-composeyml"&gt;in this article&lt;/a&gt;, so that they no longer appear in the configuration files. The repository is also located separately on the &lt;a href="https://git.schallbert.de/schallbert/lectures" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Gitea server instance&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Nevertheless, only what I consciously want to make public should be connected to the Internet.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-12-20_repo_commithistory.jpg" alt="Image: The repository&amp;#39;s commit history is public anyways"&gt;&lt;/figure&gt;&#10;&lt;p&gt;The image section from &lt;em&gt;Gitea&lt;/em&gt; shows the same commit as the warning from &lt;em&gt;Repo Lookout&lt;/em&gt;.&lt;/p&gt;&#10;&lt;h3 id="how-it-came-about"&gt;How it came about&lt;/h3&gt;&#10;&lt;p&gt;In the deploy pipeline for my subdomain &lt;code&gt;lectures.schallbert.de&lt;/code&gt; and landing page &lt;code&gt;schallbert.de&lt;/code&gt; I have a direct checkout from the &lt;em&gt;Gitea&lt;/em&gt; instance to the web server &lt;em&gt;Caddy&lt;/em&gt;. This starts automatically as soon as the &lt;code&gt;main&lt;/code&gt; branches receive an update. The runner starts a &lt;a href="https://github.com/marketplace/actions/checkout" target="_blank" rel="noopener noreferrer" class="external-link"&gt;checkout action&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, which copies the repository to the corresponding directory on the web server.&lt;/p&gt;&#10;&lt;h3 id="checkout-action-also-copies-the-git-folder"&gt;Checkout action also copies the &lt;code&gt;.git&lt;/code&gt; folder&lt;/h3&gt;&#10;&lt;p&gt;The &lt;code&gt;.git&lt;/code&gt; folder is simply set up as well. This is where all the data required by the version control software &lt;a href="https://git-scm.com/docs/gitrepository-layout" target="_blank" rel="noopener noreferrer" class="external-link"&gt;for state management&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; of the repo is stored. I cannot see the copying process itself on &lt;em&gt;Gitea&lt;/em&gt;, because the hidden &lt;code&gt;.git&lt;/code&gt; folder does not even appear in the directory there. Understandable, because the entire display on &lt;em&gt;Gitea&lt;/em&gt; is based on this folder&amp;rsquo;s content.&lt;/p&gt;&#10;&lt;p&gt;So the unwanted behavior remained under my radar - and according to &lt;em&gt;Repo Lookout&lt;/em&gt; I am by no means the only one who has this problem.&lt;/p&gt;&#10;&lt;h2 id="option-1-fix-on-the-web-server"&gt;Option 1: Fix on the web server&lt;/h2&gt;&#10;&lt;p&gt;The most obvious solution is to block access to the file on the server side. This costs few resources and is easy to set up.&lt;/p&gt;&#10;&lt;p&gt;This forum entry shows how to do it: &lt;a href="https://caddy.community/t/v2-hide-entire-folder-caddyfile/7234" target="_blank" rel="noopener noreferrer" class="external-link"&gt;hide-entire-folder-caddyfile&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. For the files I want to protect, I add the following entries to the &lt;code&gt;Caddyfile&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-yml" data-lang="yml"&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;&#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:#ae81ff"&gt;respond /.git/* &amp;#34;Access denied&amp;#34; 403&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;respond /.gitea/* &amp;#34;Access denied&amp;#34; 403&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This tells Caddy that when any file &lt;code&gt;/*&lt;/code&gt; in the folder &lt;code&gt;/.git&lt;/code&gt; is called, it should respond with the error code &lt;code&gt;403&lt;/code&gt; &amp;ldquo;Forbidden&amp;rdquo;. The wildcard (&lt;code&gt;*&lt;/code&gt;) is absolutely necessary, otherwise &lt;em&gt;only the folder itself&lt;/em&gt; and not the files it contains will be locked.&lt;/p&gt;&#10;&lt;p&gt;To check, I check what happens when I request the Git logs:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;# Terminal&#10;curl &amp;lt;lectures.schallbert.de&amp;gt;/.git/logs/HEAD&#10;Access denied &#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Works as designed.&lt;/p&gt;&#10;&lt;h2 id="option-2-fix-in-the-checkout-action"&gt;Option 2: Fix in the checkout action&lt;/h2&gt;&#10;&lt;p&gt;There is, however, a more elegant solution: ensure beforehand in the pipeline that the folder does not appear on the server at all.&lt;/p&gt;&#10;&lt;h3 id="option-1-using-sparse-checkout"&gt;Option 1: Using &lt;code&gt;sparse-checkout&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;&lt;a href="https://stackoverflow.com/questions/33933702/git-checkout-except-one-folder" target="_blank" rel="noopener noreferrer" class="external-link"&gt;sparse-checkout&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; allows you to select folders and files that should be part of the checkout. All other files in the repository remain untouched and do not appear in the branch. This saves time and storage space, especially with large repositories. But of course it only makes sense if you already know in advance that not all files need to be touched.&lt;/p&gt;&#10;&lt;h3 id="negative-list-for-sparse-checkout"&gt;Negative list for &lt;code&gt;sparse-checkout&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;In my case, I don&amp;rsquo;t want to copy the folders mentioned above to the server using checkout, but I want to copy everything else. How do I do that? Using negation in &lt;code&gt;no-cone&lt;/code&gt; mode.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;The user has explicitly said &amp;lsquo;I want these directories and not those directories.&amp;rsquo;&amp;rdquo; - Derrick Stolee, Microsoft, on &lt;a href="https://github.com/git/git/commit/55dfcf9591b088ce60ec80eb5425dda18223cac0" 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;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;The &lt;a href="https://github.com/marketplace/actions/checkout" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Checkout Action Guide&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; states that &lt;code&gt;sparse-checkout&lt;/code&gt; is also supported for the action automated by the runner.&lt;/p&gt;&#10;&lt;p&gt;Now I program with the help of the &lt;a href="https://github.github.com/actions-cheat-sheet/actions-cheat-sheet.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Github Actions Cheet Sheet&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-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /.gitea/workflows/deploy-lectures.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:#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;steps&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;CHECKOUT ---&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;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/checkout@v4&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;with&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;path&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;./tmp&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;sparse-checkout&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;&#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; /*&#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; !.git&#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; !.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;sparse-checkout-cone-mode&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:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To clarify: The script for publishing to my web server uses the action &lt;code&gt;checkout&lt;/code&gt;, subfunction &lt;code&gt;sparse-checkout&lt;/code&gt; and includes all files in the folder in the root directory &lt;code&gt;tmp&lt;/code&gt; and below except &lt;code&gt;.git&lt;/code&gt; and &lt;code&gt;.gitea&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="what-is-no-cone-mode"&gt;What is &lt;code&gt;no-cone mode&lt;/code&gt;?&lt;/h3&gt;&#10;&lt;p&gt;By default, &lt;code&gt;sparse-checkout&lt;/code&gt; expects a list of folders to include for checkout. In &lt;code&gt;no-cone&lt;/code&gt; mode, a list of patterns is expected instead. All operators that can also be used in the &lt;code&gt;.gitignore&lt;/code&gt; to specify files, folders, omissions, etc. are possible here. This allows me to exclude certain folders, but has some &lt;a href="https://git-scm.com/docs/git-sparse-checkout#_internalsnon_cone_problems" target="_blank" rel="noopener noreferrer" class="external-link"&gt;significant disadvantages&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Due to the much higher complexity of the pattern commands, the associated susceptibility to errors and the significantly more computationally intensive evaluation for larger repositories, the use of the &lt;code&gt;no-cone&lt;/code&gt; mode is not recommended and is listed as &amp;ldquo;deprecated&amp;rdquo; in the documentation. Nevertheless, the proof is in the pudding!&lt;/p&gt;&#10;&lt;h3 id="test-with-sparse-checkout"&gt;Test with &lt;code&gt;sparse-checkout&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;Now I upload the action to my Gitea instance and let my &lt;em&gt;runner&lt;/em&gt; run it once. Then I log in to the web server and check whether the &lt;code&gt;.git&lt;/code&gt; folder was created or not:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;lectures# ls -la&#10;[...]&#10;drwxr-xr-x 8 root root 4096 Dec 20 10:41 .git&#10;[...]&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Damn, the folder is still there. I look in the logs of the action on my Gitea instance:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;[...]&#10;hint: &#9;git branch -m &amp;lt;name&amp;gt;&#10;Initialized empty Git repository in /workspace/schallbert/lectures/tmp/.git/&#10;[...]&#10;::group::Setting up sparse checkout&#10;[command]/usr/bin/git config core.sparseCheckout true&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So it&amp;rsquo;s not because of &lt;code&gt;sparse-checkout&lt;/code&gt;. It&amp;rsquo;s because of the way &lt;em&gt;checkout&lt;/em&gt; works: Obviously, the &lt;code&gt;.git&lt;/code&gt; folder is absolutely necessary for setting up the repository properly on my web server. So the only option I have is to delete it automatically after checking out.&lt;/p&gt;&#10;&lt;h3 id="option-2-rm--rf"&gt;Option 2: &lt;code&gt;rm -rf&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;And so I try it by force:&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/workflows/deploy-lectures.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:#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;steps&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;CHECKOUT ---&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;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/checkout@v4&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;with&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;path&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;./tmp&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;REMOVE TEMPORARY FILES ---&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;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;&#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; rm -rfv ./tmp/.git ./tmp/.gitea&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And finally the &lt;code&gt;.git&lt;/code&gt; folder no longer appears on my web server and my &amp;ldquo;repo leak&amp;rdquo; is patched. Thanks again to &lt;em&gt;Repo Lookout&lt;/em&gt;!&lt;/p&gt;&#10;&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;&#10;&lt;p&gt;I had the problem that the hidden &lt;code&gt;.git&lt;/code&gt; folder, where the configuration and structure of repositories are stored, was published on my web server unintentionally and without my knowledge.&lt;/p&gt;&#10;&lt;p&gt;I have presented two working options for solving the problem here:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Set up an access ban on the web server&lt;/li&gt;&#10;&lt;li&gt;Modify the pipeline so that it automatically deletes the &lt;code&gt;.git&lt;/code&gt; folder after it has been rolled out.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;The second option is a bit more complex to implement, but it gets to the root of the problem instead of just fixing the symptoms. It also corresponds to the first principle of data protection: data minimization takes precedence over protective measures.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;What doesn&amp;rsquo;t exist cannot be lost&amp;rdquo; - Schallbert&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;</description></item><item><title>Logrotate-Recursion</title><link>https://blog.schallbert.de/en/logrotate-mistake/</link><pubDate>Mon, 18 Nov 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/logrotate-mistake/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-11-18_logrotate-recursion.jpg"&#10; class="post-cover"&#10; alt="Image: Logrotate creating .1 recursively"&#10; title="Logrotate-Recursion" /&gt;&#10;&lt;h2 id="what-isnt-working"&gt;What isn&amp;rsquo;t working?&lt;/h2&gt;&#10;&lt;p&gt;I actually wanted to use &lt;a href="https://github.com/logrotate/logrotate" target="_blank" rel="noopener noreferrer" class="external-link"&gt;logrotate&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; to clean up my logs daily and thus implement data economy: This way, IP addresses that I need for &lt;a href="https://blog.schallbert.de/en/server-protection/"&gt;banning&lt;/a&gt; can be deleted uniformly and automatically after a short time. Otherwise, I&amp;rsquo;m not interested in the server logs at all and they just steal storage space.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-11-18_logrotate-recursion.jpg" alt="Image: The way logrotate does the rotation looks all too recursive."&gt;&lt;/figure&gt;&#10;&lt;p&gt;With my current configuration, &lt;code&gt;logrotate&lt;/code&gt; creates one log file per day, but names it &lt;code&gt;xyz.log.1, xyz.log.1.1, xyz.log.1.1.1&lt;/code&gt; instead of the expected &lt;code&gt;xyz.log.1, xyz.log.2, xyz.log.3&lt;/code&gt;. This means that the deletion routine no longer works and the number of logs keeps growing.&lt;/p&gt;&#10;&lt;h2 id="and-why"&gt;And why?&lt;/h2&gt;&#10;&lt;p&gt;I suspect that my &lt;em&gt;logrotate&lt;/em&gt; configuration is faulty. At the moment it looks like this:&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;## LOGROTATE file named &amp;#39;blog&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;blog/* {&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;# it&amp;#39;s ok if the file doesn&amp;#39;t already exist&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;missingok&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;# Sets the logs to rotate in intervals&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;daily&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;# Tells the system to remove old logs and only keep the most recent rotated logs&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;rotate 7&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;# Rotated logs will be compressed and kept on disk if they are 10 MB or less.&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;size 10M&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;# compress and delaycompress: These two options are used together and indicate that &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;# rotated logs should be compressed (gzip) except for the most recent one.&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;compress&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:#ae81ff"&gt;delaycompress&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;As usual, the first thing I do is consult the manual, which I call up in the console using &lt;code&gt;man logrotate&lt;/code&gt;.&#10;It says that the file to be rotated can be specified directly (and not just selected using a wildcard &lt;code&gt;*&lt;/code&gt;). In addition, a log behavior can also be applied to multiple file paths by placing them in front of the curly bracket, separated by spaces.&lt;/p&gt;&#10;&lt;p&gt;The asterisk seems to be causing exactly my problem with the cascading logs: This not only rotates the actual target file &lt;code&gt;access.log&lt;/code&gt;, but also touches all the files that have already been rotated again.&lt;/p&gt;&#10;&lt;h2 id="the-solution"&gt;The solution&lt;/h2&gt;&#10;&lt;p&gt;According to the manual, I rewrite the processing instructions for Logrotate so that instead of five separate instructions for the respective logs, I only create two files. These are also simpler and shorter than the original version above.&lt;/p&gt;&#10;&lt;p&gt;Example:&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;# rotate webserver log files.&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/access.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;blog/access.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;landing/access.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;lectures/access.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:#75715e"&gt;# Sets the logs to rotate in intervals&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;daily&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;# Tells the system to remove old logs and only keep the most recent rotated logs&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;rotate 7&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;# compress and delaycompress: These two options are used together and indicate that &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;# rotated logs should be compressed (gzip) except for the most recent one.&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;compress&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;delaycompress&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="configuration-update"&gt;Configuration update&lt;/h3&gt;&#10;&lt;p&gt;I can roll this out using my existing server configuration pipeline by simply pushing it into the repository on the server.&#10;Now I just have to manually copy the files there to &lt;code&gt;/etc/logrotate.d&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="testing-logrotate"&gt;Testing Logrotate&lt;/h3&gt;&#10;&lt;p&gt;Now it would be nice if I could test whether the changes I made actually work. To do this, I use the &lt;em&gt;logrotate&lt;/em&gt; user manual again and type:&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;logrotate -d -v &amp;lt;LOGROTATE-DESCRIPTOR-FILE&amp;gt;&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;With the &lt;em&gt;debug&lt;/em&gt; and &lt;em&gt;verbose&lt;/em&gt; flags activated, logrotate will apply the rotation rule specified in the configuration file and provide feedback, but will not actually make any changes or rotations on the file system. For me, the result of the test looks something like this:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;lectures/access.log &#10; after 1 days (7 rotations)&#10;empty log files are rotated, old logs are removed&#10;considering log gitea/access.log&#10; Now: 2024-11-28 17:19&#10; Last rotated at 2024-11-27 00:00&#10; log needs rotating&#10;considering log blog/access.log&#10; Now: 2024-11-28 17:19&#10; Last rotated at 2024-11-16 00:00&#10; log needs rotating&#10;considering log landing/access.log&#10; Now: 2024-11-28 17:19&#10; Last rotated at 2024-11-07 00:00&#10; log needs rotating&#10;considering log lectures/access.log&#10; Now: 2024-11-28 17:19&#10; Last rotated at 2024-09-21 00:00&#10; log needs rotating&#10;rotating log gitea/access.log, log-&amp;gt;rotateCount is 7&#10;[...]&#10;renaming lectures/access.log.7.gz to lectures/access.log.8.gz (rotatecount 7, logstart 1, i 7), &#10;renaming lectures/access.log.6.gz to lectures/access.log.7.gz (rotatecount 7, logstart 1, i 6), &#10;renaming lectures/access.log.5.gz to lectures/access.log.6.gz (rotatecount 7, logstart 1, i 5), &#10;renaming lectures/access.log.4.gz to lectures/access.log.5.gz (rotatecount 7, logstart 1, i 4), &#10;renaming lectures/access.log.3.gz to lectures/access.log.4.gz (rotatecount 7, logstart 1, i 3), &#10;renaming lectures/access.log.2.gz to lectures/access.log.3.gz (rotatecount 7, logstart 1, i 2), &#10;renaming lectures/access.log.1.gz to lectures/access.log.2.gz (rotatecount 7, logstart 1, i 1), &#10;renaming lectures/access.log.0.gz to lectures/access.log.1.gz (rotatecount 7, logstart 1, i 0)&#10;[...]&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The output shows that all the desired log files are taken into account by &lt;em&gt;logrotate&lt;/em&gt; and also that the rotation is carried out correctly on a daily basis.&lt;/p&gt;&#10;&lt;p&gt;Learned something new again 😃&lt;/p&gt;&#10;</description></item><item><title>Klingenbutler</title><link>https://blog.schallbert.de/en/klingenbutler/</link><pubDate>Mon, 14 Oct 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/klingenbutler/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-10-14_klingenbutler-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: First prototype of Klingenbutler"&#10; title="Klingenbutler" /&gt;&#10;&lt;h2 id="why-klingenbutler"&gt;Why Klingenbutler?&lt;/h2&gt;&#10;&lt;p&gt;After I had solved the problem of the lack of a shelf for solid shampoos in my shower - the result was &lt;a href="https://blog.schallbert.de/en/seifenbutler-pilot-run/"&gt;Seifenbutler&lt;/a&gt; - I noticed razors flying around there more and more as a nuisance.&lt;/p&gt;&#10;&lt;p&gt;If they lie on the edge of the shower, a puddle forms around them. This creates a lot of limescale stains and makes cleaning more difficult. If you put them on the fittings, they will fall down sooner or later and get damaged. The same applies to the wobbly shelf on the edge of the shower cubicle.&lt;/p&gt;&#10;&lt;p&gt;It makes sense to reuse the Seifenbutler&amp;rsquo;s clamping mechanism. This means that only the &amp;ldquo;front part&amp;rdquo; needs to be redesigned and I have a stylish shower tray for razors - the &lt;em&gt;Klingenbutler&lt;/em&gt;.&lt;/p&gt;&#10;&lt;h2 id="the-design"&gt;The design&lt;/h2&gt;&#10;&lt;p&gt;I like the round shape of Seifenbutler and want to design Klingenbutler in a similar way. However, the razors should not be touched too easily by accident. So I decide to attach them as &amp;ldquo;inside&amp;rdquo; as possible. To do this, I create a ring with an opening, which in turn has three notches for razors. I design the radii of the notches so that all the wet razors I have available fit in there and even toothbrushes can be hung on the Klingenbutler without the bristles touching.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-10-14_klingenbutlercad.jpg" alt="Image: CAD drawing of Klingenbutler V1.0"&gt;&lt;/figure&gt;&#10;&lt;p&gt;I design the opening (in the drawing on the left) for upper and lower shells so that the upper shell always covers the lower shell in all possible closed positions. This avoids protruding edges with particularly large or small shower rod diameters.&lt;/p&gt;&#10;&lt;h3 id="snapping-the-razors-into-place"&gt;Snapping the razors into place&lt;/h3&gt;&#10;&lt;p&gt;It was more difficult to plan snap-in aids so that the razors cannot come loose on their own. In the first draft, I decided on a notch in the shape of a &lt;a href="https://en.wikipedia.org/wiki/Tetrahedron" target="_blank" rel="noopener noreferrer" class="external-link"&gt;double tetrahedron&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. In the picture above, the notch is labeled &amp;ldquo;drain&amp;rdquo;. It has a gradient of &lt;code&gt;3mm&lt;/code&gt; over a length of approx. &lt;code&gt;15mm&lt;/code&gt;, allows splash water to run off and enables the razors to dry quickly.&lt;/p&gt;&#10;&lt;h3 id="g2-curves"&gt;G2 curves&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--left"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-10-14_twodifferentradii.jpg" alt="Image: two different radii joined. There is a visible kink due to differences in slope at that point."&gt;&lt;/figure&gt;&#10;When I showed one of my designs to a friend of mine shortly before starting production - he is a 3D designer by profession - he immediately noticed that I had not planned for a smooth transition between curve radii. He said that in such cases &amp;ldquo;G2 curves&amp;rdquo; are used in CAD and that the model would have much more harmonious transitions.&lt;/p&gt;&#10;&lt;p&gt;In technical jargon, this seems to be referred to as &lt;a href="https://en.wikipedia.org/wiki/Smoothness#Geometric_continuity" target="_blank" rel="noopener noreferrer" class="external-link"&gt;geometric continuity&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. I immediately asked the software company behind my CAD tool whether they &lt;a href="https://hobbyline.info/forum/index.php?thread/822-wie-mache-ich/&amp;amp;postID=11565#post11565" target="_blank" rel="noopener noreferrer" class="external-link"&gt;supported something like this&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and lo and behold - there is the tool &lt;em&gt;Create tangent continuous connection&lt;/em&gt;. From now on I will use this tool when I connect radii.&lt;/p&gt;&#10;&lt;p&gt;The tool creates a &lt;a href="https://en.wikipedia.org/wiki/B%C3%A9zier_curve" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Bezier curve&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; whose endpoints are tangentially connected to the two circle segments and thus eliminate the kink.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--left"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-10-14_twodifferentradiig2.jpg" alt="Image: radii joined with tangential continuity using a Bezier Curve"&gt;&lt;/figure&gt;&#10;In addition to the more pleasing appearance, this approach also has an advantage in production: When traveling along bends, the CNC machine has to make sudden changes in direction, similar to connected line segments that meet at an angle. This means that it has to slow down and accelerate at the new curve radius. By using tangent-continuous connections, however, acceleration peaks are significantly reduced and the machine runs at a more consistent speed.&lt;/p&gt;&#10;&lt;p&gt;If you have projects with lots of &amp;ldquo;bends&amp;rdquo; in the drawing, like with the &lt;em&gt;Klingenbutler&lt;/em&gt;, you can quickly add up to a few seconds of production time per part, which can easily be saved - perhaps more on this later.&lt;/p&gt;&#10;&lt;h3 id="first-prototype---version-10"&gt;First prototype - version 1.0&lt;/h3&gt;&#10;&lt;p&gt;I started production with my &amp;ldquo;classic&amp;rdquo; prototype material: cast PMMA (acrylic glass) with a thickness of &lt;code&gt;5mm&lt;/code&gt; in opaque white. If you use reasonably suitable parameters, the material behaves extremely well. At the same time, it mercilessly reveals all errors: here a bevel a little too wide, there slight chatter marks caused by the machine accelerating out of a corner, chips caught in the bottom of threads - all of this is clearly evident in the slightly translucent material composition and strong reflections on the material edges.&#10;This material is therefore ideally suited to improving the design and production of products.&lt;/p&gt;&#10;&lt;p&gt;Thanks to my experience with Seifenbutler, holding it down during production and assembly of Klingenbutler went smoothly. From now on, toothbrushes and razors can be found in a fixed place in our shower 🙂.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-10-14_firstproto.jpg" alt="Image: First prototype of Klingenbutler V1.0"&gt;&lt;/figure&gt;&#10;&lt;h2 id="version-11---production-in-bamboo"&gt;Version 1.1 - Production in bamboo&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-10-14_bambus.jpg" alt="Image: First time Bamboo on my CNC"&gt;&lt;/figure&gt;&#10;I actually want to get away from plastic. Over the course of the year I have already switched the production of my Seifenbutlers to &amp;ldquo;greencast&amp;rdquo; acrylic glass - i.e. PMMA, which itself is made from 100% recycled material - but it would be even better not to use any plastic at all.&lt;/p&gt;&#10;&lt;p&gt;For this I had a few samples of &amp;ldquo;Moso bamboo&amp;rdquo; sent to me: stick glued wood cross-glued in three layers, similar to plywood, just under &lt;code&gt;7.5mm&lt;/code&gt; thick. For production in bamboo, I made the fitting tolerances a little more generous (&lt;code&gt;0.1mm&lt;/code&gt; undersize) and simply converted metric threads in the plan into simple holes for wood screws.&lt;/p&gt;&#10;&lt;h3 id="omitting-unnecessary-things"&gt;Omitting unnecessary things&lt;/h3&gt;&#10;&lt;p&gt;When assembling the first prototype, it became clear to me that the third bayonet had become superfluous. Together with a few other smaller changes, I developed version 1.1:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Omitting the third screw connection in the locking circle&lt;/li&gt;&#10;&lt;li&gt;Locking lugs/drain channel: Continuous engraving without stopping at &amp;ldquo;keyhole&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;Bayonet: Chamfering the insides on the back and thus breaking through the surface&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="speed-and-feed"&gt;Speed ​​and feed&lt;/h3&gt;&#10;&lt;p&gt;For full groove milling, I use a DLC-coated (diamond-like-carbon) &lt;code&gt;4mm&lt;/code&gt; two-tooth milling cutter without twist with &lt;code&gt;12mm&lt;/code&gt; cutting length. I operate it at &lt;code&gt;S30000rpm&lt;/code&gt; with a feed of &lt;code&gt;F3000mm/min&lt;/code&gt;. I go into the full material depth with &lt;code&gt;Z+ 7.5mm&lt;/code&gt; and drill holes and material ramps at &lt;code&gt;20°&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;I make bevels using a &lt;code&gt;90°&lt;/code&gt; engraving cutter, three-edged, diameter &lt;code&gt;10mm&lt;/code&gt;. I operate it at &lt;code&gt;F5000mm/min&lt;/code&gt; at &lt;code&gt;S15000rpm&lt;/code&gt; and a center distance of &lt;code&gt;2mm&lt;/code&gt;, which for a smooth edge with a width of &lt;code&gt;0.5mm&lt;/code&gt; already means that it needs &lt;code&gt;2.5mm&lt;/code&gt; of space &amp;ldquo;downwards&amp;rdquo; in the Z direction.&lt;/p&gt;&#10;&lt;p&gt;For holes, pockets and small recesses I use a relief-ground &lt;code&gt;2mm&lt;/code&gt; two-cutter fishtail with &lt;code&gt;15°&lt;/code&gt; twist and &lt;code&gt;20mm&lt;/code&gt; cutting length. I operate it at a cautious &lt;code&gt;1700mm/min&lt;/code&gt; at &lt;code&gt;S30000rpm&lt;/code&gt; and a depth of only &lt;code&gt;Z+1.5mm&lt;/code&gt;. Here, too, I dive into the material at &lt;code&gt;20°&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="production"&gt;Production&lt;/h3&gt;&#10;&lt;p&gt;Working with the material &amp;ldquo;bamboo&amp;rdquo;, which was completely unknown to me until then, was surprisingly easy. Thanks to my new and very sharp cutters, there were no tears to complain about and the material itself also had hardly any defects or damage.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;iframe&#10; src="https://makertube.net/videos/embed/ayYr653wLVLzVpmiMkhyz8"&#10; title="Making of Klingenbutler"&#10; loading="lazy"&#10; allow="autoplay; fullscreen; picture-in-picture"&#10; allowfullscreen&#10; style="width: 100%; height: auto; min-height: 315px; aspect-ratio: 16/9;"&#10; &gt;&lt;/iframe&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Making of Klingenbutler&lt;/span&gt;&lt;a href="https://makertube.net/videos/embed/ayYr653wLVLzVpmiMkhyz8" class="attr-link" target="_blank" rel="noopener noreferrer"&gt;↗&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;During processing, quite fine chips were created, which are a bit reminiscent of dust from processing MDF boards. I am not yet sure whether I should reduce the speed or whether this is due to the straight cutting edges of the cutter. When milling full slot, the cutter may have to carry chips with it for several revolutions before it can leave them behind.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-10-14_klingenbutler.jpg" alt="Image: Klingenbutler V1.1 Bamboo edition"&gt;&lt;/figure&gt;&#10;&lt;p&gt;In any case, drilling with this cutter and unchanged parameters is not recommended. All of the drill holes have charred edges, which suggests that the speed was far too high. I also drilled in one piece instead of retracting the cutter several times. Let&amp;rsquo;s use the rule of three to make this obvious: At a feed rate of &lt;code&gt;FZ+2000mm/min&lt;/code&gt;, &lt;code&gt;S30000/rpm&lt;/code&gt; and &lt;code&gt;Z7.5mm&lt;/code&gt;, the cutter makes &lt;code&gt;112.5&lt;/code&gt; revolutions in the drill hole, so the cutting edges rub senselessly along the wall of the drill hole 225 times - only the center cutting edge at the bottom removes material. In addition, the chips are not ejected but are carried along the whole time.&lt;/p&gt;&#10;&lt;p&gt;For the next prototype, I will either make drill holes with retractions and a drastically reduced speed or simply use a smaller cutter.&lt;/p&gt;&#10;&lt;h3 id="result"&gt;Result&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-10-14_bambusdetail.jpg" alt="Image: Klingenbutler detail view (underside)"&gt;&lt;/figure&gt;&#10;This is impressive. I took the bevel for edge breaking from the production in acrylic glass. It is very narrow at &lt;code&gt;0.5mm&lt;/code&gt;. Nevertheless, the detail is really clean. The upper and lower shells of Klingenbutler can be smoothly moved against each other and the clamp, my problem child &lt;a href="https://makertube.net/w/64TS5d5cp2eNqf8SSPvY77" target="_blank" rel="noopener noreferrer" class="external-link"&gt;from this video&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, could also be milled without tearing.&lt;/p&gt;&#10;&lt;p&gt;Assembly is problem-free and the wood screws can also be easily screwed into the pre-drilled holes. The bamboo Klingenbutler variant holds on to the shower rail even better than its acrylic glass counterpart - thanks to the rougher surface and greater material thickness.&lt;/p&gt;&#10;&lt;p&gt;It remains to be seen whether the material is durable enough in damp rooms. It is still popular for cutting boards in the kitchen, so I guess it will be just fine.&lt;/p&gt;&#10;&lt;h2 id="outlook"&gt;Outlook&lt;/h2&gt;&#10;&lt;p&gt;The use of bamboo has left me very motivated: a great material. Hard and strong but light, easy to machine, dimensionally stable and hardly any material defects: that&amp;rsquo;s how you want to work with natural materials!&lt;/p&gt;&#10;&lt;p&gt;This prototype is now in our shower for endurance testing. In the meantime, I&amp;rsquo;ll continue to refine the design.&lt;/p&gt;&#10;&lt;h3 id="version-12"&gt;Version 1.2&lt;/h3&gt;&#10;&lt;p&gt;I have the following small improvements planned for V1.2:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Drilling: Choose a smaller cutter (at the expense of the cycle time)&lt;/li&gt;&#10;&lt;li&gt;Fastening: Mill a cutout so that the knurled wheel no longer protrudes&lt;/li&gt;&#10;&lt;li&gt;Remove superfluous recess on the inside of the upper shell&lt;/li&gt;&#10;&lt;li&gt;Allow different radii to be merged with G2 curves&lt;/li&gt;&#10;&lt;li&gt;Replace locking lugs with a more harmonious, round drainage channel&lt;/li&gt;&#10;&lt;li&gt;Align the grain: Manufacture all parts &amp;ldquo;vertically&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;Mill the clamps in the first setup and make them &lt;code&gt;0.04mm&lt;/code&gt; thicker&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Here too, the aim is to achieve a good product through continuous improvement.&lt;/p&gt;&#10;</description></item><item><title>Broken NVME hard disk?</title><link>https://blog.schallbert.de/en/broken-nvme/</link><pubDate>Tue, 10 Sep 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/broken-nvme/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-09-10_nvme-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: My NVME-SSD that suddenly broke down"&#10; title="Broken NVME hard disk?" /&gt;&#10;&lt;h2 id="the-event"&gt;The event&lt;/h2&gt;&#10;&lt;p&gt;Mid-April 2024:&lt;/p&gt;&#10;&lt;p&gt;After a system update, my laptop suddenly wouldn&amp;rsquo;t boot. The bootloader &lt;em&gt;grub-core&lt;/em&gt; reports that it is having problems writing sectors.&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;error: ../grub-core/efi/efidisk.c:638: failure writing sector 0xXXXXXXXX&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;When I reboot, the screen remains black after the company logo is displayed with a blinking cursor. If I look behind the mask, loading various services fails:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2024-09-10_nvme-boot-errors.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Boot errors due to read-only nvme&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="initial-analysis"&gt;Initial analysis&lt;/h2&gt;&#10;&lt;p&gt;I suspected a problem with the computer update and carried out a hard drive test from Lenovo&amp;rsquo;s &lt;a href="https://en.wikipedia.org/wiki/UEFI" target="_blank" rel="noopener noreferrer" class="external-link"&gt;UEFI&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Result:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;The media check is complete, the result is: FAIL&#10;&lt;/code&gt;&lt;/pre&gt;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-09-10_diskcheck-fail.jpg" alt="Image: disk check tool diagnoses a defective device"&gt;&lt;/figure&gt;&#10;&lt;p&gt;It&amp;rsquo;s not a problem with the operating system, but rather with the disk. Damn. Do I have to worry about my data? I do have backups lying around, but the last one is three weeks old, so I might be missing several hours of work. A setup from blank will take me a lot of time. So I&amp;rsquo;m going to try to see if anything can be salvaged.&lt;/p&gt;&#10;&lt;h3 id="self-test"&gt;Self-test&lt;/h3&gt;&#10;&lt;p&gt;So I run the &lt;a href="https://en.wikipedia.org/wiki/Self-Monitoring,_Analysis_and_Reporting_Technology" target="_blank" rel="noopener noreferrer" class="external-link"&gt;SMART&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; tests for the disk with &lt;code&gt;smartctl -x /dev/nvm0&lt;/code&gt; and now know that it counts about &lt;code&gt;7600&lt;/code&gt; hours of operation and has &lt;code&gt;240GB&lt;/code&gt; of usable capacity. The disk used under the name &lt;code&gt;nvme0n1&lt;/code&gt; thinks it is now &amp;ldquo;read-only&amp;rdquo;. All partitions including the kernel. Great. The result of my online search was something like:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;The disk is irreparably damaged. A new one is needed.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h3 id="repair-attempt"&gt;Repair attempt&lt;/h3&gt;&#10;&lt;p&gt;You don&amp;rsquo;t have to believe everything you read online. So I use Lenovo&amp;rsquo;s own repair tool for hard drives. It runs from the UEFI screen and is called &lt;a href="https://download.lenovo.com/pccbbs/thinkvantage_en/userguide_uefi_v04.08.000.pdf" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Bad Block Recovery Tool&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. The program scans the disk for defective sectors and attempts to restore affected blocks. The run takes almost half an hour for the last 10%.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-09-10_badblockrecovery_summary.jpg" alt="Image: Bad Block Recovery tool&amp;#39;s summary for my disk"&gt;&lt;/figure&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-09-10_badblockrecovery_detail.jpg" alt="Image: Bad Block Recovery tool&amp;#39;s output messages"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Result: Recovery failed. I run the tool again. And a third time in slow &lt;code&gt;pedantic&lt;/code&gt; mode. Again and again to no avail. I keep getting &lt;code&gt;critical medium error&lt;/code&gt;.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-09-10_badblockrecovery_log.jpg" alt="Image: Bad Block Recovery tool&amp;#39;s log"&gt;&lt;/figure&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;Is all of this not working because the disk has switched to read-only mode?&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;As I can no longer boot and have no console available, my options with on-board resources are now exhausted.&lt;/p&gt;&#10;&lt;h3 id="comment-the-read-only-mode-for-ssds"&gt;Comment: The read-only mode for SSDs&lt;/h3&gt;&#10;&lt;p&gt;According to the Internet &lt;a href="https://www.disktuna.com/a-write-protected-ssd-nvme-read-only/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;(DiskTuna)&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, the &lt;em&gt;read only mode&lt;/em&gt; is there to limit damage. If the controller of the storage device detects a serious error, it puts the disk into read-only access. This minimizes the risk of further blocks being corrupted or killed.&lt;/p&gt;&#10;&lt;p&gt;In the forum &lt;a href="https://superuser.com/questions/1802470/ssd-in-read-only-mode-after-power-failure" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Superuser&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; someone describes an error identical to mine, and there is actually nothing that can be done to keep the medium working.&lt;/p&gt;&#10;&lt;h2 id="data-recovery-using-a-boot-stick"&gt;Data recovery using a boot stick&lt;/h2&gt;&#10;&lt;p&gt;I continue with a USB stick that I convert to a Linux boot stick at another computer. I plug it into my laptop and can finally boot it up again. I connect an external hard drive as data recovery medium.&lt;/p&gt;&#10;&lt;h3 id="get-an-overview-with-fdisk"&gt;Get an overview with &lt;em&gt;fdisk&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;First I check whether all disks and partitions appear. To do this I type the command &lt;code&gt;sudo fdisk -l&lt;/code&gt; into the console.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-09-10_fdisk.jpg" alt="Image: fdisk&amp;#39;s output on my live system"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Luckily, everything is still there! Maybe I can just copy the files from the defective hard drive?&lt;/p&gt;&#10;&lt;h3 id="copying-with-cp"&gt;Copying with &lt;em&gt;cp&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Let&amp;rsquo;s try it out.&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;sudo cp -r -v /mnt/nvme0n1p3/Documents/my/important/files /mnt/externalHdd/save&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For a few seconds I think everything is going well. But then &lt;code&gt;cp&lt;/code&gt; stops with an error message saying the volume is not available. In fact, the disk is no longer mounted. However, I can find it again and mount it straight away. A few attempts later, I have backed up my really important data, but the copying process always stops after a few seconds. This would take far too long for the entire disk.&lt;/p&gt;&#10;&lt;p&gt;The observed behavior - mount, copy for a few seconds, disk disappearing - was confirmed to me in a few related forum posts online, so it seems to be normal behavior.&lt;/p&gt;&#10;&lt;p&gt;I need another solution.&lt;/p&gt;&#10;&lt;h3 id="save-data-with-ddrescue"&gt;Save data with &lt;em&gt;ddrescue&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-09-10_ddrescue.jpg" alt="Image: ddrescue output when saving my broken SSD&amp;#39;s files to an external HDD."&gt;&lt;/figure&gt;&#10;After a quick online search, I installed the program &lt;a href="https://www.gnu.org/software/ddrescue/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ddrescue&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;The documentation states some things very clearly. For example, that you should understand exactly what it does before you use it. Or the following:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Never try to repair a file system on a drive with I/O errors; you will probably lose even more data. &amp;quot; - &lt;a href="https://www.gnu.org/software/ddrescue/manual/ddrescue_manual.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ddrescue Manual&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Now I&amp;rsquo;m wondering whether I made the same mistake with the block repair attempt above. Or am I on the safe side with Bad Block Recovery, since this is about data blocks on the disk and not the &amp;ldquo;overlying&amp;rdquo; file system?&lt;/p&gt;&#10;&lt;p&gt;It also says here that the so-called &lt;em&gt;mapfile&lt;/em&gt; is a core function of the program. It offers a great time advantage when aborting, restarting, merging backups and during data recovery. Therefore, it is not a bad idea to create the mapfile when creating an offline backup and to put it away safely. Overall, I found the tool extremely helpful, effective and easy to use and can only recommend it at this point.&lt;/p&gt;&#10;&lt;p&gt;Back to action: I find a very simple command to copy the entire data partition of my damaged NVME storage to an external hard drive:&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;ddrescue --sparse /dev/nvme0n1p3 /run/media/liveuser/myExternalHDD/datapartition&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;--sparse&lt;/code&gt; option skips areas of the source disk that are filled with zeros and can thus save a lot of space on the target disk.&lt;/p&gt;&#10;&lt;p&gt;The program takes a quarter of an hour to complete the entire run. As you can see in the picture above, it can actually save &lt;code&gt;99.6%&lt;/code&gt; of the data and even several months later I haven&amp;rsquo;t missed the remaining &lt;code&gt;0.4%&lt;/code&gt;. Lucky me.&lt;/p&gt;&#10;&lt;h3 id="verification-view-the-output-of-ddrescue"&gt;Verification: View the output of &lt;em&gt;ddrescue&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;In any case, the program ultimately saves a single file on my external disk: &lt;code&gt;datapartition.img&lt;/code&gt;&#10;I can view this image with &lt;code&gt;fdisk -l&lt;/code&gt;:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;datapartition.img: 236.89 GiB, 254356226048 bytes, 496789504 sectors&#10;Units: sectors of 1 * 512 = 512 bytes&#10;Sector size (logical/physical): 512 bytes / 512 bytes&#10;I/O size (minimum/optimal): 512 bytes / 512 bytes&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Everything is there, that&amp;rsquo;s good. Is all data included as well?&lt;/p&gt;&#10;&lt;p&gt;To check this, I mount the partition and look around using &lt;code&gt;cd&lt;/code&gt; and &lt;code&gt;ls -l&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo mkdir /mnt/olddisk &lt;span style="color:#75715e"&gt;# Mount point of the rescued data image&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo mount /run/media/liveuser/myExternalHDD/datapartition.img /mnt/olddisk&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Fortunately, everything that should be there was there. Now I could unmount the image, shut down the computer and have a new hard drive installed.&lt;/p&gt;&#10;&lt;h2 id="the-new-hard-drive"&gt;The new hard drive&lt;/h2&gt;&#10;&lt;p&gt;Here, too, I was lucky: my used laptop was still under warranty and so I got it returned within a week with a new (and twice as big) hard drive. Because I was so afraid of data theft, I removed my old hard drive before sending it. I felt pretty paranoid.&lt;/p&gt;&#10;&lt;p&gt;I could have saved myself this work, maybe, because &lt;em&gt;ddrescue&lt;/em&gt; has a software solution for paranoid people like me: the &lt;code&gt;--fill-mode&lt;/code&gt; option.&#10;This allows you to delete the &amp;ldquo;still good&amp;rdquo; blocks and sectors after the rescue has been completed. Only the sectors that are already defective remain. From the &lt;a href="https://www.gnu.org/software/ddrescue/manual/ddrescue_manual.html#Fill-mode" target="_blank" rel="noopener noreferrer" class="external-link"&gt;online documentation&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ddrescue --fill-mode&lt;span style="color:#f92672"&gt;=&lt;/span&gt;+ --force /dev/zero bad_drive mapfile&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;m not sure whether this would have worked with a disk like mine, which the on-board controller had set to &amp;ldquo;read-only&amp;rdquo;. Whatever.&lt;/p&gt;&#10;&lt;p&gt;At least I can now continue writing blog entries with a fully functional laptop ☺️.&lt;/p&gt;&#10;</description></item><item><title>Gitea act_runner: Jump-start issues</title><link>https://blog.schallbert.de/en/fix-gitea-runner/</link><pubDate>Fri, 30 Aug 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/fix-gitea-runner/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-08-30_badgateway_runner-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Bad Gateway error message from Gitea&amp;#39;&amp;#39;s act_runner at startup"&#10; title="Gitea act_runner: Jump-start issues" /&gt;&#10;&lt;aside class="update-box update-box--warn" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ⚠️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Gitea Retires `act_runner`&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2026-09-15T00:00:00Z"&gt;&#10; 2026-09-15&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; This article refers to an Actions implementation by Gitea, the &lt;code&gt;act_runner&lt;/code&gt;. It is derived from &lt;a href="https://github.com/nektos/act" target="_blank" rel="noopener noreferrer" class="external-link"&gt;nectos/act&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Gitea now uses &lt;a href="https://blog.gitea.com/release-of-runner-1.0.0/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;its own runner&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. The old runner should be replaced. More info: Read my post to &lt;a href="https://blog.schallbert.de/en/build-deploy-hugo-with-actions-docker-caddy/"&gt;deploy hugo with Gitea Actions, docker, and caddy&lt;/a&gt;&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;h2 id="problem-statement"&gt;Problem statement&lt;/h2&gt;&#10;&lt;p&gt;Since I run this page myself I experience problems starting &lt;em&gt;Gitea&lt;/em&gt; and &lt;em&gt;act_runner&lt;/em&gt;. Sometimes, the runner won&amp;rsquo;t start. It exits with status &lt;code&gt;-1&lt;/code&gt; and so my workflows wouldn&amp;rsquo;t run the website build, verification and deploy tasks.&lt;/p&gt;&#10;&lt;p&gt;The error message is always the same:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;[...]&#10;runner-1 | time=&amp;#34;2024-08-26T10:09:46Z&amp;#34; level=info msg=&amp;#34;Starting runner daemon&amp;#34;&#10;runner-1 | time=&amp;#34;2024-08-26T10:09:46Z&amp;#34; level=error msg=&amp;#34;fail to invoke Declare&amp;#34; error=&amp;#34;unavailable: 502 Bad Gateway&amp;#34;&#10;runner-1 | Error: unavailable: 502 Bad Gateway&#10;runner-1 exited with code 1&#10;gitea | 2024/08/26 10:09:46 cmd/web.go:242:runWeb() [I] Starting Gitea on PID: 16&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Ok, the runner cannot hook onto &lt;em&gt;Gitea&lt;/em&gt;. When I restart all services via &lt;code&gt;docker restart&lt;/code&gt;, the &lt;em&gt;act_runner&lt;/em&gt; container tells me it would be missing a network with ID &lt;code&gt;&amp;lt;long hexcode ID&amp;gt;&lt;/code&gt;&lt;/p&gt;&#10;&lt;h2 id="interim-corrective-action"&gt;Interim corrective action&lt;/h2&gt;&#10;&lt;p&gt;I didn&amp;rsquo;t find time for resolving this issue. So I just ran docker compose twice:&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:#75715e"&gt;# schallbert server-console&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker compose -f /path/to/giteas/composefile up -d&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The second time, &lt;em&gt;act_runner&lt;/em&gt; would start successfully (because Gitea is ready):&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;gitea | 2024/08/26 10:38:17 routers/init.go:116:InitWebInstalled() [I] Git version: 2.45.2 (home: /data/gitea/home)&#10;runner-1 | time=&amp;#34;2024-08-26T10:38:22Z&amp;#34; level=info msg=&amp;#34;Starting runner daemon&amp;#34;&#10;runner-1 | time=&amp;#34;2024-08-26T10:38:22Z&amp;#34; level=info msg=&amp;#34;runner: action-runner, with version: v0.2.10, with labels: [ubuntu-latest], declare successfully&amp;#34;&#10;runner-1 exited with code 0&#10;runner-1 | time=&amp;#34;2024-08-26T10:40:32Z&amp;#34; level=info msg=&amp;#34;Started runner daemon&amp;#34;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Not very satisfying to do things twice. Plus, I did this manually. So why is that?&lt;/p&gt;&#10;&lt;h2 id="root-cause-analysis"&gt;Root cause analysis&lt;/h2&gt;&#10;&lt;p&gt;First I thought it was due to &lt;em&gt;Caddy&lt;/em&gt; not being ready as it runs the reverse proxy, connecting &lt;em&gt;Gitea&lt;/em&gt; to the internet. In reality, &lt;em&gt;Gitea&lt;/em&gt; is the culprit: At the time, &lt;em&gt;docker&lt;/em&gt; starts &lt;em&gt;act_runner&lt;/em&gt;, its startup procedure wouldn&amp;rsquo;t be complete. So I try to find out how to manage dependencies in docker.&lt;/p&gt;&#10;&lt;h2 id="solution-reflecting-dependencies"&gt;Solution: Reflecting dependencies&lt;/h2&gt;&#10;&lt;p&gt;First, I try linking gitea to the runner in the docker compose file.&lt;/p&gt;&#10;&lt;h3 id="depends_on"&gt;depends_on&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-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;## service: runner&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;depends_on&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;condition&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;service_started&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Well, still act_runner seems to be starting early. Docker just makes sure it has started the container, and does not wait for it to return a &lt;code&gt;healthy&lt;/code&gt; signal.&lt;/p&gt;&#10;&lt;h3 id="healthcheck"&gt;Healthcheck&lt;/h3&gt;&#10;&lt;p&gt;So I have to make sure that gitea&amp;rsquo;s startup procedure is complete. For this, docker provides the condition &lt;code&gt;service_healthy&lt;/code&gt;. I adjust the runner configuration like so:&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;## service: runner&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;depends_on&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;condition&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;service_healthy&lt;/span&gt; &lt;span style="color:#75715e"&gt;# required so runner can attach to 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:#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;The &lt;code&gt;service_healthy&lt;/code&gt; qualifier is returned by the &lt;code&gt;healthcheck&lt;/code&gt; function. It consists of a so-called &lt;code&gt;test&lt;/code&gt; and some environment parameters that define intervall, number of retries, delay and timeout conditions for checking service ready.&lt;/p&gt;&#10;&lt;p&gt;For the test I chose a simple command, assuming that Gitea (and, implicitly, Caddy too) would be healthy once it is able to respond to a GET request from &lt;code&gt;curl&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-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;## service: gitea&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;healthcheck&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;test&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;CMD&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;curl&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;-f&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;https://git.schallbert.de/&amp;#34;&lt;/span&gt;] &lt;span style="color:#75715e"&gt;# checks if gitea is available&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;interval&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;10s&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;retries&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;3&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;start_period&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;30s&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;timeout&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;10s&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With these lines, docker checks if gitea is &amp;ldquo;healthy&amp;rdquo; and only then starts &lt;em&gt;act_runner&lt;/em&gt;. This permanently solved the problem and both &lt;em&gt;Gitea&lt;/em&gt; and &lt;em&gt;act_runner&lt;/em&gt; are stable.&lt;/p&gt;&#10;</description></item><item><title>Sending error logs</title><link>https://blog.schallbert.de/en/server-deploy-logging/</link><pubDate>Tue, 20 Aug 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/server-deploy-logging/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-08-20_deploy_logging-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: A notification message sent on error"&#10; title="Sending error logs" /&gt;&#10;&lt;p&gt;In this article I&amp;rsquo;ll look at how to set up &amp;ldquo;monitoring&amp;rdquo; for my server. Applications and services should be able to send me notifications in the event of an error.&lt;/p&gt;&#10;&lt;h2 id="what-is-this-about"&gt;What is this about?&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Learn about and select sending mechanisms&lt;/li&gt;&#10;&lt;li&gt;Write test messages and verify the automation&lt;/li&gt;&#10;&lt;li&gt;Automatically send error report from &lt;em&gt;Borgmatic&lt;/em&gt;&lt;/li&gt;&#10;&lt;li&gt;Send runner logs through &lt;em&gt;Gitea&lt;/em&gt;&lt;/li&gt;&#10;&lt;li&gt;Notification when logging into my server via &lt;em&gt;ssh&lt;/em&gt;&lt;/li&gt;&#10;&lt;li&gt;Create and send logs for server updates / server errors&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="verification-and-monitoring"&gt;Verification and monitoring&lt;/h3&gt;&#10;&lt;p&gt;After running an automation, I want to know whether it was carried out successfully and whether all programs and services started their work as expected. This should apply to any automation - whether it is specifically sending an update, creating automatic backups or an action from &lt;em&gt;Gitea&lt;/em&gt;, it doesn&amp;rsquo;t matter.&lt;/p&gt;&#10;&lt;p&gt;Normally I would use reporting mechanisms from &lt;em&gt;act_runner&lt;/em&gt; for something like this. In the case of a server update, the runner is not available due to the &lt;a href="https://blog.schallbert.de/en/server-config-version-control/#preliminary-considerations"&gt;circular reference&lt;/a&gt; already mentioned in the article &lt;a href="https://blog.schallbert.de/en/server-config-deploy/"&gt;Rolling out the server configuration&lt;/a&gt;, as all applications have to be shut down temporarily.&lt;/p&gt;&#10;&lt;p&gt;In addition, applications may have their own procedures for monitoring. So I have to look at mechanisms that allow me to easily access the information.&lt;/p&gt;&#10;&lt;h3 id="what-characterizes-good-monitoring-for-me"&gt;What characterizes good monitoring for me?&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;It is unobtrusive, so it only reports in the event of an error or unusual occurrence.&lt;/li&gt;&#10;&lt;li&gt;It provides specific information and error messages that are easy to understand.&lt;/li&gt;&#10;&lt;li&gt;It uses a message channel that works even if the system being monitored crashes.&lt;/li&gt;&#10;&lt;li&gt;It presents reports and error messages in isolation from other topics and does not mix anything.&lt;/li&gt;&#10;&lt;li&gt;It is brief.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="methods-of-automatic-reporting"&gt;Methods of automatic reporting&lt;/h2&gt;&#10;&lt;p&gt;I will break this section down. In the general part, I will discuss the on-board tool for asynchronous monitoring that is available to me on the Ubuntu server. After that, I will look at the solutions that are partly built into my services or the applications that are compatible with them. I do not want to limit myself to the classic tool of email, but also look at &amp;ldquo;more modern&amp;rdquo; communication channels such as messenger programs or RSS feeds.&lt;/p&gt;&#10;&lt;h3 id="mail-via-console---curl"&gt;Mail via console - &lt;em&gt;curl&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Sending emails as notifications is common practice in many companies. With Linux, this can usually be done without any additional programs: The standard &lt;a href="https://curl.se/docs/manpage.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;curl&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; can help here.&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;curl&lt;/em&gt; is a program for transferring data from or to a server. If I enter my blog as the target, I get the HTML page output as a text file on the console:&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;curl https://blog.schallbert.de&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This made me notice how much unnecessary data my blog software generates. I&amp;rsquo;ll have to clean that up later. Back to the topic: You can also use &lt;em&gt;curl&lt;/em&gt; to access any web backend - for example a mail server:&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:#75715e"&gt;# copied from https://stackoverflow.com/questions/8260858/how-to-send-email-from-terminal&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl --url &lt;span style="color:#e6db74"&gt;&amp;#39;smtps://smtp.gmail.com:465&amp;#39;&lt;/span&gt; --ssl-reqd &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --mail-from &lt;span style="color:#e6db74"&gt;&amp;#39;from-email@gmail.com&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --mail-rcpt &lt;span style="color:#e6db74"&gt;&amp;#39;to-email@gmail.com&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --user &lt;span style="color:#e6db74"&gt;&amp;#39;from-email@gmail.com:YourPassword&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -T &amp;lt;&lt;span style="color:#f92672"&gt;(&lt;/span&gt;echo -e &lt;span style="color:#e6db74"&gt;&amp;#39;From: from-email@gmail.com\nTo: to-email@gmail.com\nSubject: Curl Test\n\nHello&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This only works for my mail provider if I allow logins from external clients. Google, for example, calls these &amp;ldquo;less secure apps&amp;rdquo;. As described in my post &lt;a href="https://blog.schallbert.de/en/server-config-version-control/#lets-get-to-work"&gt;Server configuration with Git&lt;/a&gt;, I&amp;rsquo;m not a fan of writing secrets into anything - so I would rather not use the direct route via &lt;em&gt;curl&lt;/em&gt;.&lt;/p&gt;&#10;&lt;h3 id="mail-via-console---mail-mailx-mailutils-swaks"&gt;Mail via console - &lt;em&gt;mail&lt;/em&gt;, &lt;em&gt;mailx&lt;/em&gt;, &lt;em&gt;mailutils&lt;/em&gt;, &lt;em&gt;swaks&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;If you don&amp;rsquo;t want to always provide all the configuration information for the server and secrets, there are various handy tools for the console such as &lt;a href="https://mailutils.org/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;mailutils&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; or &lt;a href="https://github.com/jetmore/swaks" target="_blank" rel="noopener noreferrer" class="external-link"&gt;swaks&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Here, the connection to the mail server is configured once using the tool and can be stored in environment variables, for example. The syntax varies from program to program, but an email always drops out at the end.&lt;/p&gt;&#10;&lt;p&gt;The only major disadvantage for me is that information domains are mixed up. I would not want another &amp;ldquo;report thread&amp;rdquo; in my emails that I&amp;rsquo;d have to search for in the mass of messages. Other notification media in contrast allow me to set an automatic expiration date so that they disappear from my list after a set time.&lt;/p&gt;&#10;&lt;h3 id="create-rss-feed"&gt;Create RSS feed&lt;/h3&gt;&#10;&lt;p&gt;Unusual but possible: I could create the monitoring as an &lt;a href="https://en.wikipedia.org/wiki/RSS" target="_blank" rel="noopener noreferrer" class="external-link"&gt;RSS feed&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; like my blog (&lt;a href="https://blog.schallbert.de/en/index.xml"&gt;schallberts-blog-feed&lt;/a&gt;) has e.g. using a Jekyll instance and put it online as a website. This would be easy to subscribe to, were readable with practically any reader and I could even set it up separately for each application. But there are obvious disadvantages:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;High effort: &lt;em&gt;Gitea&lt;/em&gt; runner with &lt;em&gt;Jekyll&lt;/em&gt; instance, web server and subdomain required.&lt;/li&gt;&#10;&lt;li&gt;Publicly available: Suddenly build processes, updates, upgrades and error messages are accessible to everyone.&lt;/li&gt;&#10;&lt;li&gt;Error-prone: If &lt;em&gt;Gitea&lt;/em&gt;, &lt;em&gt;act_runner&lt;/em&gt;, my proxy or the web server crashes, I don&amp;rsquo;t get any reports.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;With the last point at the latest, this option is out of the question for me. I want to get a report when my applications don&amp;rsquo;t do what they&amp;rsquo;re supposed to.&#10;Let&amp;rsquo;s take a look at the applications I already run and see how they tackle this problem.&lt;/p&gt;&#10;&lt;h3 id="borgmatic"&gt;&lt;em&gt;borgmatic&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Borgmatic is compatible with a lot of &lt;a href="https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;monitoring options&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. These include &lt;a href="https://github.com/caronc/apprise" target="_blank" rel="noopener noreferrer" class="external-link"&gt;apprise&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://ntfy.sh/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ntfy&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://healthchecks.io/docs/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;healthchecks&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://cronitor.io/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;cronitor&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://www.pagerduty.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;pagerduty&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://cronhub.io/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;cronhub&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and &lt;a href="https://grafana.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;grafana&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;em&gt;Apprise&lt;/em&gt; is a library. It is open source and can be integrated into an existing application as a dependency. Like an adapter, it enables asynchronous communication between the application and various communication services such as SMS, mail, messenger (e.g. &lt;a href="https://signal.org/de/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Signal&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;), various home automation systems or the notification mechanism of various operating systems. The latter, however, only works on the local machine. The trigger for communication must always come from the application.&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;ntfy&lt;/em&gt; is a push notification service. It is open source and can be self-hosted or used as a service via web application. &lt;em&gt;Apprise&lt;/em&gt;, for example, supports &lt;em&gt;ntfy&lt;/em&gt; as a communication service. The structure is quite simple and works like &lt;a href="https://de.wikipedia.org/wiki/MQTT" target="_blank" rel="noopener noreferrer" class="external-link"&gt;MQTT&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; via publication subscription / broker client mechanism, but HTTP-based.&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;Healthchecks&lt;/em&gt; is a service. It is open source and can be self-hosted. The service is there to monitor regular activities and can act as a dead man&amp;rsquo;s switch: If, contrary to expectations, there is no response from the monitored program, it can raise an error message itself. This can in turn be forwarded to various communication services.&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;Cronitor&lt;/em&gt; is a monitoring solution and web application that, in addition to the notifications I need, provides a lot of analysis tools, performance measurements and metrics - mostly for money. There is a free &amp;ldquo;hacker&amp;rdquo; account with limited functionality, but this tool is also far too big and complex for me.&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;PagerDuty&lt;/em&gt; sees itself as a commercial &amp;ldquo;operations&amp;rdquo; platform that provides &amp;ldquo;incident management&amp;rdquo;, automation, &amp;ldquo;business operations&amp;rdquo;, &amp;ldquo;AIOps&amp;rdquo; etc. It&amp;rsquo;s out for me straight away. At the latest when I read the word &amp;ldquo;AIOps&amp;rdquo; 😅&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;cronhub&lt;/em&gt; looks like a commercial web application to me that, like &lt;em&gt;Healthchecks&lt;/em&gt;, can create cron jobs, monitor them and report errors. It is of no interest to me because it does not seem to be open source and I could not host it myself.&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;Grafana&lt;/em&gt; is an open source web application that can either be self-hosted or used as a cloud service. Although many larger companies and projects use the application, it is orders of magnitude too extensive and feature-rich for my purposes.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="fail2ban"&gt;&lt;em&gt;fail2ban&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Fail2ban does not have an automation solution like &lt;em&gt;borgmatic&lt;/em&gt;. It simply creates log files that need to be evaluated in order to obtain content for notifications. At the moment I cannot think of anything that I absolutely need to know about Fail2ban. So I am not creating any reports for this for now.&lt;/p&gt;&#10;&lt;h3 id="gitea"&gt;&lt;em&gt;Gitea&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Gitea allows the log files to be configured very precisely. Access logs can be written out separately from service logs, repository logs or action logs and then processed further. According to my research, Gitea only offers a &lt;a href="https://docs.gitea.com/next/administration/config-cheat-sheet#mailer-mailer" target="_blank" rel="noopener noreferrer" class="external-link"&gt;mailer&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; as a notification system. In my opinion, this is primarily intended for repository and action logs.&lt;/p&gt;&#10;&lt;p&gt;Here, too, it would be most beneficial for me to analyze the logs and create a report myself if necessary.&lt;/p&gt;&#10;&lt;h3 id="server"&gt;Server&lt;/h3&gt;&#10;&lt;p&gt;I already roll out my server configuration files using a script. So, in the event of an error, I could redirect the logs to a file and attach them to a report in any channel.&lt;/p&gt;&#10;&lt;p&gt;In addition, successful logins on the server would be worth a message. Then I can immediately determine whether it was me or not.&lt;/p&gt;&#10;&lt;h2 id="selecting-the-reporting-program"&gt;Selecting the reporting program&lt;/h2&gt;&#10;&lt;p&gt;I not only have to select a monitoring program, but also choose a communications service through which the reports are sent.&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;Program&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Advantage&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Disadvantage&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;em&gt;curl&lt;/em&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;simple&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;configuration must be provided&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;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;works super easily with &lt;em&gt;ntfy&lt;/em&gt;&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;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;No monitoring if the server crashes&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;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Onboard tools&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&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;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;no service required&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&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;&lt;em&gt;Apprise&lt;/em&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;enables integration of the Signal API&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;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Complex in interaction with external services&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;&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;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;no service required&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&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;&lt;em&gt;Healthchecks&lt;/em&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Can act as a watchdog/dead man switch&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Registration required&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;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Can be hosted locally&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Local hosting contradicts the watchdog concept&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;I only have a small server with a few applications, so I keep it as simple as possible and choose &lt;em&gt;Apprise&lt;/em&gt; for services like &lt;em&gt;borgmatic&lt;/em&gt;, which come with this library anyway, and &lt;em&gt;curl&lt;/em&gt; for those whose reporting mechanics I have to program myself.&lt;/p&gt;&#10;&lt;h3 id="communication-channel"&gt;Communication channel&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;Program&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Advantage&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Disadvantage&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;SMS&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;High reliability&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Registration required&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;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Limited to a few characters&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;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;No attachments possible&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;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;No topics, domain mixing&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;Mail&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;easy to set up&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Poor searchability&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;RSS feed&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Good sortability&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Complicated to set up, error-prone&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;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Client very lightweight&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Local hosting contradicts watchdog concept&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;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Content publicly available&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;&lt;em&gt;ntfy&lt;/em&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Simple and lightweight&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Registration optional for web use&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;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Client purpose-oriented&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Insecure: No encryption without registration&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;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Free for small users&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&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;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Independent of your own machine&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&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;&lt;em&gt;Signal&lt;/em&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&amp;ldquo;Note to self&amp;rdquo; easy to set up&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Setting up and configuring the Signal API complex&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;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Independent of your own machine&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Topics somewhat difficult to implement&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;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Useless if the server crashes&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;SMS, mail and RSS feed are already out of the question for me due to some of the disadvantages already explained above. So that leaves &lt;em&gt;ntfy&lt;/em&gt; and &lt;em&gt;Signal&lt;/em&gt;. &lt;em&gt;ntfy&lt;/em&gt; impresses with its simplicity: send an HTTP PUSH request to a self-defined &amp;ldquo;topic&amp;rdquo; and subscribe to it on your cell phone - done. It is also easy to expand, because with self-hosting I can later increase security if necessary (encryption) and control the sending behavior. &lt;em&gt;Signal&lt;/em&gt;, on the other hand, requires a separate client with relatively complex configuration. In addition, the devices would have to be connected to the server and cell phone, so it is not easy to add new subscribers. On the other hand, the transmission is well secured end-to-end, I do not have to register and it is also free.&lt;/p&gt;&#10;&lt;p&gt;For now, I have decided to go for the less complex solution with &lt;em&gt;ntfy&lt;/em&gt;.&lt;/p&gt;&#10;&lt;h2 id="logging"&gt;Logging&lt;/h2&gt;&#10;&lt;p&gt;Good. Now it&amp;rsquo;s clear that I&amp;rsquo;m calling home using &lt;em&gt;curl&lt;/em&gt; and &lt;em&gt;apprise&lt;/em&gt; via the &lt;em&gt;ntfy&lt;/em&gt; service. Let&amp;rsquo;s see what content needs to be transmitted and how to make it as unobtrusive as possible, but still short and concise.&lt;/p&gt;&#10;&lt;h3 id="when-should-logs-be-sent"&gt;When should logs be sent?&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;em&gt;borgmatic&lt;/em&gt;: If a backup fails&lt;/li&gt;&#10;&lt;li&gt;If I successfully log in to my server or Gitea&lt;/li&gt;&#10;&lt;li&gt;After rolling out an update to the server configuration&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;Docker&lt;/em&gt;: If an application fails or doesn&amp;rsquo;t start&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;Gitea&lt;/em&gt;: Failed run of &lt;em&gt;act_runner&lt;/em&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="what-should-i-include-in-a-report"&gt;What should I include in a report?&lt;/h3&gt;&#10;&lt;p&gt;I want the classic &amp;ldquo;W questions&amp;rdquo; answered.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;When did it happen (time stamp)?&lt;/li&gt;&#10;&lt;li&gt;Which application is reporting?&lt;/li&gt;&#10;&lt;li&gt;What happened?&lt;/li&gt;&#10;&lt;li&gt;Where (module, line of code) etc. did it happen?&lt;/li&gt;&#10;&lt;li&gt;How many were injured (severity, recovery)?&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;A notification then looks something like this:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;TIMESTAMP APPLICATION PRIORITY MESSAGE EFFECT DETAIL&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="rough-version-of-a-report"&gt;Rough version of a report&lt;/h3&gt;&#10;&lt;p&gt;An &lt;em&gt;ntfy&lt;/em&gt; message could look something like this:&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;curl &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Title: Error Borgmatic&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Priority: urgent&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Tags: warning&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -d &lt;span style="color:#e6db74"&gt;&amp;#34;YYYY-MM-DD HH:MM:SS Backup creation aborted. Access to repository blocked&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ntfy.sh/schallberts-topic&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-08-20_ntfytest.jpg" alt="Image: First ntfy test on my phone app sent with curl"&gt;&lt;/figure&gt;&#10;I install the corresponding app on my phone, register on the &amp;ldquo;Topic&amp;rdquo; and send the message. It&amp;rsquo;s nice when things just work!&#10;Aha, the app shows the time of receipt. That&amp;rsquo;s accurate enough for me.&lt;/p&gt;&#10;&lt;h2 id="implementation"&gt;Implementation&lt;/h2&gt;&#10;&lt;p&gt;Here I&amp;rsquo;ll take a look at all the services for which I&amp;rsquo;d like to set up notifications one by one.&lt;/p&gt;&#10;&lt;h3 id="borgmatic-1"&gt;&lt;em&gt;borgmatic&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Let&amp;rsquo;s start with a pilot test in small steps. First I configure Borgmatic to send a message via &lt;em&gt;Apprise&lt;/em&gt; to &lt;em&gt;ntfy&lt;/em&gt; if the backup creation fails:&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;# /borgmatic.d/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:#f92672"&gt;on_error&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;echo &amp;#34;Error while creating a backup.&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;apprise -vv --title &amp;#34;Borgmatic Error&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;body &amp;#34;Could not run {output}. Aborted {error}.&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;ntfy://schallberts-topic&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then I test the command by calling &lt;em&gt;apprise&lt;/em&gt; within the Borgmatic container. And indeed, it works. But getting here took me an hour, as the &lt;code&gt;yml&lt;/code&gt; with its syntax rules even interprets within strings and &lt;em&gt;borgmatic&lt;/em&gt; constantly refused to read the configuration file due to &lt;code&gt;:&lt;/code&gt; and &lt;code&gt;-&lt;/code&gt; characters. If you see an error similar to this one:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;At &amp;#39;on_error[1]&amp;#39;: {&amp;#39;apprise -vv --title &amp;#34;Borgmatic Error&amp;#34; --body &amp;#34;Could not run {output}&amp;#39;: &amp;#39;Aborted {error}.&amp;#34; ntfy://schallberts-topic&amp;#39;} is not of type &amp;#39;string&amp;#39;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This shows that the interpretation of characters or indentations went wrong and the punctuation needs to be checked. Alternatively, the pipe operator &lt;code&gt;|&lt;/code&gt; can be used to combine a command. Reference: &lt;a href="https://yaml.org/spec/1.2-old/spec.html#id2795688" target="_blank" rel="noopener noreferrer" class="external-link"&gt;yml specification&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;h3 id="server-1"&gt;Server&lt;/h3&gt;&#10;&lt;p&gt;An application of &lt;em&gt;ntfy&lt;/em&gt; to monitor logins on a server can already be found in the &lt;a href="https://docs.ntfy.sh/examples/#ssh-login-alerts" target="_blank" rel="noopener noreferrer" class="external-link"&gt;documentation of &lt;em&gt;ntfy&lt;/em&gt; itself&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. The description shows that something like this is easy to implement yourself and gives a great example using Pluggable Authentication Modules (&lt;a href="https://en.wikipedia.org/wiki/Linux_PAM" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PAM library&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;).&lt;/p&gt;&#10;&lt;p&gt;Insert the following code at the end of the &lt;code&gt;sshd&lt;/code&gt; file in the &lt;code&gt;etc/pam.d&lt;/code&gt; directory:&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;session optional pam_exec.so /usr/bin/ntfy-ssh-login.sh&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This line tells &lt;em&gt;PAM&lt;/em&gt; that when a &lt;code&gt;session&lt;/code&gt; is opened via &lt;code&gt;ssh&lt;/code&gt;, it should call the executing module &lt;code&gt;pam_exec&lt;/code&gt;, which then runs the script specified below. The value &lt;code&gt;optional&lt;/code&gt; means that the configuration file should continue to be run even if the action fails. More details on how to use &lt;em&gt;PAM&lt;/em&gt; can be found, for example, on &lt;a href="https://www.baeldung.com/linux/pam-ssh-login-notifications" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Baeldung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Then you simply have to store the &lt;em&gt;ntfy&lt;/em&gt; call in the &lt;code&gt;ntfy-ssh-login.sh&lt;/code&gt; script when the script determines that PAM has detected an &lt;code&gt;open_session&lt;/code&gt; event.&lt;/p&gt;&#10;&lt;p&gt;That&amp;rsquo;s exactly how I implemented it and it works straight away. Great! The only disadvantage: I made this modification directly on the server. Without a container and outside of my configuration backup. If I now have to set up the server again for some reason, the change in &lt;em&gt;PAM&lt;/em&gt; is lost and I won&amp;rsquo;t receive any more notifications until I manually enter the change again.&lt;/p&gt;&#10;&lt;h3 id="server-configuration"&gt;Server configuration&lt;/h3&gt;&#10;&lt;p&gt;To send a report after running the configuration automation, I actually only have to adapt the &lt;a href="https://blog.schallbert.de/en/server-config-deploy/#the-finished-automation"&gt;server-config-action&lt;/a&gt; script that I wrote in the last article.&lt;/p&gt;&#10;&lt;p&gt;First, I want to be informed when the script runs without errors. To do this, I add a &lt;em&gt;curl&lt;/em&gt; command at the end of the file.&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:#75715e"&gt;# server-config-action.sh&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;# action 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;# [...]&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;# send success notification&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Title: server-config-action&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Priority: low&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Tags: white_check_mark&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -d &lt;span style="color:#e6db74"&gt;&amp;#34;Rollout successful&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ntfy.sh/schallbert-server-config-push-topic&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;However, if the script does not run without errors, I would like to do the following:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Cancel actions after the error occurs&lt;/li&gt;&#10;&lt;li&gt;Create an error log&lt;/li&gt;&#10;&lt;li&gt;Send this log&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;I can achieve the first point by adding a trap for errors:&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:#75715e"&gt;# server-config-action.sh&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;trap &lt;span style="color:#e6db74"&gt;&amp;#39;handle_error $LINENO&amp;#39;&lt;/span&gt; ERR&#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;# action commands...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This calls the function &lt;code&gt;handle_error&lt;/code&gt;. It is given the line number where the error &lt;code&gt;ERR&lt;/code&gt; occurred. It also covers errors that can occur when restarting the container. I create the error log by redirecting the output of the individual script commands. I achieve this with the following line:&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:#75715e"&gt;# server-config-action.sh&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;exec 3&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; 1&amp;gt;server-config-action.log 2&amp;gt;&amp;amp;&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;# action commands ...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With this command, any standard output &lt;code&gt;stdout&lt;/code&gt; should be passed on to the file descriptor &lt;code&gt;3&lt;/code&gt;, specified here with the log file &lt;code&gt;server-config-action-log&lt;/code&gt;, overwriting it. For &amp;ldquo;append&amp;rdquo; there would have to be two redirection operators &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt;. The log file thus replaces the console output, which I still had at this point in the previous article.&lt;/p&gt;&#10;&lt;p&gt;To have the log sent to me, I now define the following function at the beginning of the Bash script:&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:#75715e"&gt;# server-config-action.sh&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;# on error, send a notification&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;handle_error&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; &lt;span style="color:#75715e"&gt;# stop redirecting to file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; exec 1&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt; 1&amp;gt;&amp;amp;&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; curl &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Title: server-config-action FAILED&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Priority: high&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Tags: x&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -T server-config-action.log &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Filename: server-config-action.log&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ntfy.sh/schallbert-server-config-push-topic&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; exit &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;&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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# set error trap&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;# redirect stdout and stderr to file&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;# action commands...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-08-20_ntfytest_allchannels.jpg" alt="Image: Phone screenshot of my ntfy messages"&gt;&lt;/figure&gt;&#10;&lt;h3 id="gitea-1"&gt;&lt;em&gt;Gitea&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;For Github Actions reports, there is already an existing example at &lt;a href="https://docs.ntfy.sh/examples/#github-actions" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ntfy_examples/#github-actions&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. And the best part: Since the &lt;em&gt;act_runner&lt;/em&gt; from &lt;em&gt;Gitea&lt;/em&gt; is compatible with &lt;em&gt;Github Actions&lt;/em&gt; at &lt;a href="https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables" target="_blank" rel="noopener noreferrer" class="external-link"&gt;in the environment parameters&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, sending to &lt;em&gt;ntfy&lt;/em&gt; works for me straight away. In the runner&amp;rsquo;s workflow file, all you have to do is enter the &lt;em&gt;curl&lt;/em&gt; command specified on the website and you&amp;rsquo;re done.&lt;/p&gt;&#10;&lt;h2 id="result"&gt;Result&lt;/h2&gt;&#10;&lt;p&gt;Five notifications about the most important processes on my server have now been set up. I have understood the underlying mechanics and can create additional notifications at any time if I need them. If I trigger all notifications as a test, my smartphone will display the as shown.&lt;/p&gt;&#10;&lt;p&gt;But I have not yet achieved independence from the system to be monitored. All notifications come from the affected machine and there are sometimes &lt;a href="https://blog.schallbert.de/en/gitea-action-runner-jekyll-dockerimage/#reverse-proxy-connection-refused"&gt;even dependencies&lt;/a&gt; between Docker containers. For example, the &lt;em&gt;act_runner&lt;/em&gt; has to connect to &lt;em&gt;Gitea&lt;/em&gt; via websockets. And that only works if &lt;em&gt;Caddy&lt;/em&gt; provides the reverse proxy.&lt;/p&gt;&#10;&lt;p&gt;If I find out in the next few months that a service &amp;ldquo;under my radar&amp;rdquo; is no longer able to work, I will have to establish independence.&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;Context for setup with &lt;a href="https://docs.ntfy.sh/#getting-started" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ntfy&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;li id="fn:2"&gt;&#10;&lt;p&gt;Context for the &lt;a href="https://github.com/caronc/apprise/wiki/Notify_signal" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Signal API&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&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;Report by &lt;code&gt;asad-awadia&lt;/code&gt; on &lt;a href="https://blog.aawadia.dev/2023/04/24/signal-api/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;setting up the Signal API&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&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;li id="fn:4"&gt;&#10;&lt;p&gt;Healthchecks is directly available as a &lt;a href="https://hub.docker.com/r/healthchecks/healthchecks" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Docker image&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:4" 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:5"&gt;&#10;&lt;p&gt;Incidentally, the operators &lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt; and &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt; are not difficult for me to remember, because I used to have a lot to do with the programming language &lt;code&gt;C&lt;/code&gt;. There - and in many other programming languages ​​too - these are shift operators that can &amp;ldquo;shift&amp;rdquo; a value bit by bit or move one field to another. In the &lt;a href="https://en.wikipedia.org/wiki/Reduced_instruction_set_computer" target="_blank" rel="noopener noreferrer" class="external-link"&gt;RISC&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; architectures that I used at the time, the shift operation was &amp;ldquo;cheap&amp;rdquo;, i.e. very fast and memory-saving.&amp;#160;&lt;a href="#fnref:5" 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>Server configuration rollout</title><link>https://blog.schallbert.de/en/server-config-deploy/</link><pubDate>Wed, 31 Jul 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/server-config-deploy/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-07-31-server-configdeploy-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Diagram showing a config file rollout on Schallbert&amp;#39;&amp;#39;s server"&#10; title="Server configuration rollout" /&gt;&#10;&lt;p&gt;In the previous article, I &lt;a href="https://blog.schallbert.de/en/server-config-version-control/"&gt;brought my configuration files under version control&lt;/a&gt;. Now I want to automatically install the updates provided on the server.&lt;/p&gt;&#10;&lt;h2 id="what-is-this-about"&gt;What is this about?&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Writing a script to automatically detect the update trigger&lt;/li&gt;&#10;&lt;li&gt;Server applications should be shut down and a backup copy created&lt;/li&gt;&#10;&lt;li&gt;The script should distribute the configuration on the system&lt;/li&gt;&#10;&lt;li&gt;All applications should then be restarted&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="ways-and-possibilities"&gt;Ways and possibilities&lt;/h2&gt;&#10;&lt;p&gt;Here, too, I spent several hours researching. For larger projects, infrastructure experts seem to use specialized automation tools. These include &lt;a href="https://docs.ansible.com/ansible/latest/getting_started/index.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Ansible&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, simpler tools such as &lt;a href="https://www.cdi.st/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;cdist&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; or even &lt;a href="https://kubernetes.io/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Kubernetes&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; for highly scalable services.&lt;/p&gt;&#10;&lt;h3 id="automation-services-ansible-cdist-kubernetes"&gt;Automation services Ansible, cdist, Kubernetes&lt;/h3&gt;&#10;&lt;p&gt;Ansible and cdist seem to follow a similar concept: On the source machine (in this example my laptop) I create the configuration for my server and store this and the instructions for configuring my services in a &amp;ldquo;playbook&amp;rdquo; (Ansible) or in &amp;ldquo;types&amp;rdquo; (cdist).&lt;/p&gt;&#10;&lt;p&gt;To put it simply - as I understand it - the tool then takes care of building the configuration at the push of a button, dialing into the target host via &lt;code&gt;ssh&lt;/code&gt;, pushing it over there and then starting it. For Ansible, there are even tutorials like this one for my scenario with &lt;code&gt;docker-compose&lt;/code&gt;, which makes getting started even easier.&lt;/p&gt;&#10;&lt;p&gt;Kubernetes takes a different approach. It sees itself as more of a container manager, load balancer and scaling agent, but can do similar things for my purposes.&lt;/p&gt;&#10;&lt;h3 id="my-approach"&gt;My approach&lt;/h3&gt;&#10;&lt;p&gt;I, on the other hand, only need a fraction of the capabilities of these programs. I am also put off by the &amp;ldquo;additional&amp;rdquo; &lt;code&gt;ssh&lt;/code&gt; channel, the configuration effort, the additional programs sometimes required on the target system, and the necessary reading and selection of the best tool for me. Because thanks to my very simple pipeline from the last article, the configuration is already on my server. It &amp;ldquo;only&amp;rdquo; needs to be copied to the right places and the affected services restarted.&lt;/p&gt;&#10;&lt;p&gt;Therefore, I am trying to solve this problem using on-board tools, my brain in working order and a few searches in relevant forums on the topics &lt;a href="https://superuser.com/questions/181517/how-to-execute-a-command-whenever-a-file-changes" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&amp;ldquo;executing a script when a file changes&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and &lt;a href="https://www.freecodecamp.org/news/copy-a-directory-in-linux-how-to-cp-a-folder-in-the-command-line-in-linux-and-unix-macos/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&amp;ldquo;copying folder structures in Linux&amp;rdquo;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="risk-of-circular-reference"&gt;Risk of circular reference&lt;/h3&gt;&#10;&lt;p&gt;However, I am taking a risk: Since the deployment runs via Gitea, but the configuration affects Gitea itself, if there is an error in this module I can no longer change or reset anything: The Gitea service is then broken. I would have to get the configuration up and running again manually on the server.&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;ll try it out anyway and see if I am actually confronted with such a problem. If so, I&amp;rsquo;ll just switch to &lt;code&gt;cdist&lt;/code&gt; and document it in a separate article! 🤗&lt;/p&gt;&#10;&lt;h2 id="preparation-create-folder-system-and-scripts"&gt;Preparation: Create folder system and scripts&lt;/h2&gt;&#10;&lt;p&gt;I think about it for a moment and create a few folders in the server file system:&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:#75715e"&gt;# /opt/server-config&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mkdir automation-hooks-trigger&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mkdir automation-hooks-handler&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;These folders should represent the two sides of the automation. &lt;code&gt;trigger&lt;/code&gt; contains text files that can be manipulated from the (web) service side. For example, &lt;em&gt;act_runner&lt;/em&gt; should write the file &lt;code&gt;server-config-update&lt;/code&gt; as soon as an update is available.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;service &amp;ndash;&amp;gt; writes to trigger file ||| server_handler() &amp;ndash;&amp;gt; trigger_file.changed ? run_action() : loop()&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;The handler folder contains script files that make the necessary changes to the server file system.&lt;/p&gt;&#10;&lt;h2 id="implementation-shell-script-to-handle-the-update"&gt;Implementation: Shell script to handle the update&lt;/h2&gt;&#10;&lt;p&gt;In the previous article, I executed the command &lt;code&gt;touch server-config-update.txt&lt;/code&gt; in the &lt;em&gt;act_runner&lt;/em&gt; container to signal the presence of a new server configuration. On the server, I now use the following code to periodically check whether this file has changed.&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:#75715e"&gt;### set directories&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;actionfile&lt;span style="color:#f92672"&gt;=&lt;/span&gt;/opt/server-config/automation-hooks-handler/server-config-action.sh&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;triggerfile&lt;span style="color:#f92672"&gt;=&lt;/span&gt;/opt/server-config/automation-hooks-trigger/server-config-update.txt&#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;### Set initial time of file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;LTIME&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;stat -c %Z &lt;span style="color:#e6db74"&gt;${&lt;/span&gt;triggerfile&lt;span style="color:#e6db74"&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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;while&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;do&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ATIME&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;stat -c %Z &lt;span style="color:#e6db74"&gt;${&lt;/span&gt;triggerfile&lt;span style="color:#e6db74"&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;&#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; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$ATIME&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; !&lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$LTIME&lt;span style="color:#e6db74"&gt;&amp;#34;&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;then&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;${&lt;/span&gt;actionfile&lt;span style="color:#e6db74"&gt;}&lt;/span&gt; 2&amp;gt;&amp;amp;&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; LTIME&lt;span style="color:#f92672"&gt;=&lt;/span&gt;$ATIME&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;fi&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sleep &lt;span style="color:#ae81ff"&gt;10&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;done&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The operating system is given the interpreter with which the script is to be executed via &lt;a href="https://en.wikipedia.org/wiki/Shebang_%28Unix%29" target="_blank" rel="noopener noreferrer" class="external-link"&gt;#!/bin/bash&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. The command &lt;a href="https://wiki.ubuntuusers.de/stat/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;stat -c %Z&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; queries when the file was last changed and returns the time in &lt;a href="https://www.epochconverter.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Epoch format&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Finally, the time taken at the beginning of the script is compared with the time taken in the loop and if there is a change, the deploy routine that is still to be written can then run. The reference time is then updated.&#10;Finally, the script (&lt;code&gt;sleep 10&lt;/code&gt;) pauses for ten seconds before the query starts again. I use absolute paths so I&amp;rsquo;m able to both start it from console and per service &lt;code&gt;cron&lt;/code&gt; oder &lt;code&gt;systemd&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="first-test-and-hooking-into-autostart"&gt;First test and hooking into &amp;ldquo;autostart&amp;rdquo;&lt;/h3&gt;&#10;&lt;p&gt;Now we need to make the file executable for a first test:&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:#75715e"&gt;# change file mode bits: add &amp;#34;executable&amp;#34; flag to server-config-handler script&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;chmod +x /opt/server-config/automation-hooks-handler/server-config-handler.sh&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next, I create the &lt;code&gt;server-config-action.sh&lt;/code&gt; file and add just an &lt;code&gt;echo&lt;/code&gt; command. To test the setup I create my update trigger file locally and start the script. I then modify the triggerfile in another shell using &lt;code&gt;touch server-config-update.txt&lt;/code&gt; and &lt;code&gt;--- CONFIG UPDATE TRIGGER detected ---&lt;/code&gt; appears in the console. Great!&lt;/p&gt;&#10;&lt;p&gt;Later on the server, I need to have the script run automatically after a restart. To do this, I use the &lt;code&gt;cron&lt;/code&gt; tool &lt;a href="https://wiki.ubuntuusers.de/Cron/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;help&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and create a new entry using &lt;code&gt;crontab -e&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# cron can automatically execute recurring tasks&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;# in this case, we&amp;#39;re running server-config-handler script on reboot&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@reboot sh /opt/server-config/automation-hooks-handler/server-config-handler.sh&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With the command &lt;code&gt;ps aux&lt;/code&gt; I can now check whether the script is actually being executed:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;schallbert@server: ps aux&#10;[...]&#10;8:15 0:00 /bin/bash ./server-config-handler.sh&#10;8:15 0:00 sleep 10&#10;[...]&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="backup"&gt;Backup&lt;/h2&gt;&#10;&lt;p&gt;Making a backup of my applications and files before I roll out the update makes total sense. So I tell &lt;a href="https://blog.schallbert.de/en/server-protection/#regular-backups"&gt;Borg&lt;/a&gt; that I want to create a backup now. Of course, I have to stop all services first. This means that all data is accessible, coherent and static.&lt;/p&gt;&#10;&lt;h3 id="freeze-state-and-data"&gt;Freeze state and data&lt;/h3&gt;&#10;&lt;p&gt;To do this, I create a script in &lt;code&gt;automation-hooks-handler&lt;/code&gt; that automatically terminates all containers except &lt;em&gt;borg&lt;/em&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#! /bin/bash&#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;# /opt/automation-hooks-handler/backup-pre-action.sh&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;# this shell script shuts down all docker containers prior to backup&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;echo &lt;span style="color:#e6db74"&gt;&amp;#34;Shutting down containers for backup:&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;echo &lt;span style="color:#e6db74"&gt;&amp;#34;watchtower...&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cd /opt/watchtower&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker compose down 2&amp;gt;&amp;amp;&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;&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;The expression &lt;code&gt;2&amp;gt;&amp;amp;1&lt;/code&gt; means that any error output is redirected to the console. The number &lt;code&gt;1&lt;/code&gt; represents the file descriptor for &lt;code&gt;stdout&lt;/code&gt;, while &lt;code&gt;2&lt;/code&gt; means &lt;code&gt;stderr&lt;/code&gt;. The operator &lt;code&gt;&amp;gt;&amp;amp;&lt;/code&gt; functions as a &lt;code&gt;redirect merger&lt;/code&gt;. Later, we can go to this point and write the output to a log file - but I&amp;rsquo;ll leave that out for now for the sake of simplicity.&lt;/p&gt;&#10;&lt;h3 id="borgmatic-trigger-handler-mechanism-2-and-3"&gt;Borgmatic: Trigger handler mechanism #2 and #3&lt;/h3&gt;&#10;&lt;p&gt;I can now run the script in two ways:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;As a call by the &lt;code&gt;server-config-handler&lt;/code&gt; script described above&lt;/li&gt;&#10;&lt;li&gt;By the automation solution &lt;em&gt;borgmatic&lt;/em&gt; placed in front of &lt;em&gt;borg&lt;/em&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;I choose the second option and therefore write the following commands in &lt;code&gt;borgmatic.d/config.yml&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-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# borgmatic.d/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:#75715e"&gt;# List of one or more shell commands or scripts to execute before&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;# creating a backup, run once per repository.&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;before_backup&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;echo &amp;#34;Triggering container shutdown for backup.&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;touch /etc/automation-hooks-trigger/backup-pre.txt&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;sleep 20&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;echo &amp;#34;Assuming container shutdown complete. Creating the backup now.&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:#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;after_backup&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;echo &amp;#34;Triggering container restart after backup.&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;touch /etc/automation-hooks-trigger/backup-post.txt&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;sleep 10&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;echo &amp;#34;Assuming container restart complete. Exiting.&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:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For this to work properly, I have to create a volume in the associated &lt;code&gt;docker-compose.yml&lt;/code&gt; and have it point to the path in the server&amp;rsquo;s file system: &lt;code&gt;${VOLUME_UPDATE_TRIGGER}:/etc/automation-hooks-trigger&lt;/code&gt;&#10;Now I set up the other side of these triggers: Handlers monitor trigger files for changes and call action scripts accordingly. These look very identical to &lt;code&gt;server-config-handler.sh&lt;/code&gt; except &lt;code&gt;actionfile&lt;/code&gt; and &lt;code&gt;triggerfile&lt;/code&gt; paths.&lt;/p&gt;&#10;&lt;h3 id="creating-the-backup"&gt;Creating the backup&lt;/h3&gt;&#10;&lt;p&gt;If I were to address &lt;em&gt;borg&lt;/em&gt; directly, the backup could be created using &lt;code&gt;create&lt;/code&gt;. To do this, I would have to specify in which repository the backup copy should be saved and under which name. In the example below, this is specified using the scope operator: &lt;code&gt;::config-update&lt;/code&gt;. The folders to be backed up are then specified.&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;borg create /path/to/repo::config-update ~/opt&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I use &lt;em&gt;borgmatic&lt;/em&gt;, which takes a lot of work off my hands using the configuration file. However, I have to execute the command in the container. To better check whether everything is working, I output statistics &amp;ldquo;verbose&amp;rdquo; to the console (&lt;code&gt;--stats -v 1&lt;/code&gt;) and display the copied files &lt;code&gt;--files&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker exec borgmatic sh -c &lt;span style="color:#e6db74"&gt;&amp;#34;cd &amp;amp;&amp;amp; borgmatic --stats -v 1 --files 2&amp;gt;&amp;amp;1&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I add this line to the automation script.&lt;/p&gt;&#10;&lt;h2 id="second-test-to-create-the-backup"&gt;Second test to create the backup&lt;/h2&gt;&#10;&lt;p&gt;If everything works now, the complete process looks like this:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;After the changed files have been received by Giteas Automation, &lt;em&gt;act_runner&lt;/em&gt; (in the Docker container) stores the files on the server and then sets the &lt;code&gt;server-config-update&lt;/code&gt; trigger.&lt;/li&gt;&#10;&lt;li&gt;Within &lt;code&gt;10sec&lt;/code&gt; the trigger is recognized by &lt;code&gt;server-config-handler.sh&lt;/code&gt;, which then calls &lt;code&gt;server-config-action.sh&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;borgmatic&lt;/em&gt; (in the Docker container) is instructed to create a backup. This in turn writes the &lt;code&gt;pre-backup&lt;/code&gt; trigger.&lt;/li&gt;&#10;&lt;li&gt;Again within &lt;code&gt;10sec&lt;/code&gt; this &lt;code&gt;pre-backup-handler.sh&lt;/code&gt; calls the &lt;code&gt;backup-pre-action.sh&lt;/code&gt; script and stops all containers except &lt;em&gt;borgmatic&lt;/em&gt;.&lt;/li&gt;&#10;&lt;li&gt;Due to the built-in delay, &lt;em&gt;borgmatic&lt;/em&gt; waits for this and then creates the backup.&lt;/li&gt;&#10;&lt;li&gt;After the backup, &lt;em&gt;borgmatic&lt;/em&gt; writes the &lt;code&gt;backup-post-action.sh&lt;/code&gt; trigger.&lt;/li&gt;&#10;&lt;li&gt;Within another &lt;code&gt;10 seconds&lt;/code&gt;, &lt;code&gt;post-backup-handler.sh&lt;/code&gt; recognizes the changed file and restarts all containers via &lt;code&gt;post-backup-actions.sh&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;borgmatic&lt;/em&gt; tells &lt;code&gt;server-config-update.sh&lt;/code&gt; whether any errors have occurred anywhere in the process so far. If not, it continues.&lt;/li&gt;&#10;&lt;li&gt;All Docker containers are stopped.&lt;/li&gt;&#10;&lt;li&gt;The server configuration is rolled out to the appropriate locations.&lt;/li&gt;&#10;&lt;li&gt;All Docker containers are restarted with the new configuration.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;A first success: These scripts are already running on my laptop up to point 6:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;schallbert@laptop: touch server-config-update.txt&#10;server-config-handler@laptop: --- RUN backup ---&#10;borgmatic@docker: /etc/borgmatic.d/config.yml: Running 4 commands for pre-backup hook&#10; Triggering container shutdown for backup. &#10;backup-pre-handler.sh@laptop: --- RUN backup-pre-action.sh ---&#10; Shutting down containers for backup:&#10; watchtower...&#10; [...]&#10; complete.&#10;borgmatic@docker: Assuming container shutdown complete. Creating the backup now.&#10; local: Creating archive&#10; Failed to create/acquire the lock /mnt/repository/lock.exclusive&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="troubleshooting-for-failed-to-acquire-the-lock"&gt;Troubleshooting for &amp;ldquo;failed to acquire the lock&amp;rdquo;&lt;/h3&gt;&#10;&lt;p&gt;This problem occurs for me when &lt;em&gt;borg&lt;/em&gt; reports an error when creating a backup that causes the program to abort. In this case, the repository is apparently not released correctly, so that after restarting the container it remains reserved for the old, now non-existent container. The following command solves this problem:&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 exec borgmatic sh -c &lt;span style="color:#e6db74"&gt;&amp;#34;cd &amp;amp;&amp;amp; borg break-lock /mnt/repository&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="transferring-the-configuration"&gt;Transferring the configuration&lt;/h3&gt;&#10;&lt;p&gt;Now the configuration files have to be copied to the correct location on the server. Fortunately, I had already cloned the target folder structure when creating the repository, so I should be able to do this with a single copy command without &amp;ldquo;hardcoding&amp;rdquo;. After a bit of online research and a look at the user manual for the copy command &lt;code&gt;man cp&lt;/code&gt;, I have my command:&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:#75715e"&gt;# copy recursively contents of folder &amp;#34;server-config&amp;#34; to &amp;#34;/opt&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo cp -r -v /opt/server-config/. /opt 2&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This tells the operating system to copy the contents (&lt;code&gt;/.&lt;/code&gt;) of the folder &lt;code&gt;server-config&lt;/code&gt; recursively (&lt;code&gt;-r&lt;/code&gt;), including all subfolders into the folder &lt;code&gt;opt&lt;/code&gt;, which is located in the root directory &lt;code&gt;/&lt;/code&gt;. &lt;code&gt;cp&lt;/code&gt; works in an overwrite-supplement manner, so it will create files that do not yet exist and overwrite existing ones, and will not &amp;ldquo;copy them next to each other&amp;rdquo; under the same name. With the &lt;code&gt;-v&lt;/code&gt; option I can output additional details, and with &lt;code&gt;2&amp;gt;&amp;amp;1&lt;/code&gt; I redirect the error output to the console.&lt;/p&gt;&#10;&lt;p&gt;All folder indicators must be exactly where they are in the command: A slash after &lt;code&gt;/opt/&lt;/code&gt; would copy folders redundantly without overwriting, but would overwrite files. Without &lt;code&gt;.&lt;/code&gt; the folder &lt;code&gt;server-config&lt;/code&gt; would be created in the target path.&lt;/p&gt;&#10;&lt;h3 id="switching-to-rsync"&gt;Switching to rsync&lt;/h3&gt;&#10;&lt;p&gt;Unfortunately the &lt;code&gt;cp&lt;/code&gt; command also copies a few files that I don&amp;rsquo;t want copied: repository-specific folders such as &lt;code&gt;.gitea&lt;/code&gt;, or the folders for triggers and handlers. I only need these under &lt;code&gt;server-config&lt;/code&gt;, not directly in &lt;code&gt;opt&lt;/code&gt;. To fix this, I use the &lt;code&gt;rsync&lt;/code&gt; command instead. There I can use a &lt;code&gt;-u&lt;/code&gt; option so new files owerwrite older ones only and add &lt;code&gt;--exclude&lt;/code&gt; to exclude files and folders that should not be copied. This looks like so:&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;rsync -r -u -v --exclude &lt;span style="color:#e6db74"&gt;&amp;#39;.*&amp;#39;&lt;/span&gt; --exclude &lt;span style="color:#e6db74"&gt;&amp;#39;README.md&amp;#39;&lt;/span&gt; --exclude &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;lt;otherFolders&amp;gt;&amp;#39;&lt;/span&gt; /opt/server-config/. /opt 2&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But suddenly &lt;em&gt;gitea&lt;/em&gt; no longer starts. Error message:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;docker@server: [...] failed to load config file &amp;#34;app.ini&amp;#34;: open: permission denied&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After a long time of pondering and restarting the Docker client several times, I see that &lt;em&gt;rsync&lt;/em&gt; has written the permissions of the source file to the target file, which was not the case with &lt;em&gt;cp&lt;/em&gt; before: &lt;code&gt;-rw-------&lt;/code&gt;. Now I change this by running &lt;code&gt;chmod +r app.ini&lt;/code&gt;. Everything starts up again as usual! 🎉&lt;/p&gt;&#10;&lt;h3 id="restart-all-applications"&gt;Restart all applications&lt;/h3&gt;&#10;&lt;p&gt;Since I run everything on my server in Docker, two simple &lt;a href="https://docs.docker.com/reference/cli/docker/container/restart/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;commands&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; are sufficient:&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 stop &lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;docker ps -a -q&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt; 2&amp;gt;&amp;amp;&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;&lt;span style="color:#75715e"&gt;# [...roll out config changes...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker restart &lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;docker ps -a -q&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt; 2&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="the-finished-automation"&gt;The finished automation&lt;/h2&gt;&#10;&lt;p&gt;My script is now finished and simply calls the action script.&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:#75715e"&gt;#! /bin/bash&#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;#/opt/automation-hooks-handler/server-config-handler.sh&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:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;[[&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$ATIME&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; !&lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$LTIME&lt;span style="color:#e6db74"&gt;&amp;#34;&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;then&lt;/span&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34;--- CONFIG UPDATE TRIGGER detected ---&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ./server-config-action.sh 2&amp;gt;&amp;amp;&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; LTIME&lt;span style="color:#f92672"&gt;=&lt;/span&gt;$ATIME&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;fi&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sleep &lt;span style="color:#ae81ff"&gt;10&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;done&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The Script &lt;code&gt;server-config-action&lt;/code&gt; then executes the above described actions:&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:#75715e"&gt;#! /bin/bash&#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;#/opt/automation-hooks-handler/server-config-action.sh&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;echo &lt;span style="color:#e6db74"&gt;&amp;#34;--- RUN backup ---&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker exec borgmatic sh -c &lt;span style="color:#e6db74"&gt;&amp;#34;cd &amp;amp;&amp;amp; borgmatic --stats -v 1 --files 2&amp;gt;&amp;amp;1&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#34;--- STOP all containers ---&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker stop &lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;docker ps -a -q&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt; 2&amp;gt;&amp;amp;&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;echo &lt;span style="color:#e6db74"&gt;&amp;#34;--- DEPLOY config ---&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cp -r -v /opt/server-config/. /opt 2&amp;gt;&amp;amp;&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;echo &lt;span style="color:#e6db74"&gt;&amp;#34;--- RESTART all containers ---&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker restart &lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;docker ps -a -q&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt; 2&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In order for the whole thing to work reliably, the three handler scripts must run in the background:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;server-config-handler.sh&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;backup-pre-handler.sh&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;backup-post-handler.sh&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;They react to the respective triggers by &lt;em&gt;act_runner&lt;/em&gt; from Gitea or by &lt;em&gt;borgmatic&lt;/em&gt;. I will now expand the &lt;em&gt;crontab&lt;/em&gt; accordingly and then I am done with the task for now. The console output of the entire process looks like this:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;--- RUN backup ---&#10;/etc/borgmatic.d/config.yml: Running 4 commands for pre-backup hook&#10;Triggering container shutdown for backup.&#10;--- RUN backup-pre-action.sh ---&#10;Shutting down containers for backup:&#10;gitea... &#10;fail2ban...&#10;complete.&#10;Assuming container shutdown complete. Creating the backup now.&#10;local: Creating archive&#10;&amp;lt;borg archive stats&amp;gt;&#10;/etc/borgmatic.d/config.yml: Running 4 commands for post-backup hook&#10;Triggering container restart after backup.&#10;--- RUN backup-post-action.sh ---&#10;Restarting containers after backup:&#10;fail2ban...&#10;gitea... &#10;watchtower...&#10;complete.&#10;Assuming container restart complete. Exiting.&#10;local: Pruning archives&#10;local: Compacting segments&#10;compaction freed about 1.82 MB repository space.&#10;local: Running consistency checks&#10;summary:&#10;/etc/borgmatic.d/config.yml: Successfully ran configuration file&#10;--- STOP all containers ---&#10;&amp;lt;container ids&amp;gt;&#10;--- DEPLOY config ---&#10;sending incremental file list&#10;&amp;lt;files that are copied&amp;gt;&#10;sent 206,558 bytes received 3,463 bytes 420,042.00 bytes/sec&#10;total size is 193,167 speedup is 0.92&#10;--- RESTART all containers ---&#10;&amp;lt;container ids&amp;gt;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Great! Now all I need is for this log to be delivered to me if something goes wrong.&lt;/p&gt;&#10;</description></item><item><title>Server configuration with Git</title><link>https://blog.schallbert.de/en/server-config-version-control/</link><pubDate>Mon, 15 Jul 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/server-config-version-control/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-07-15-server-versioncontrol-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Diagram thumb for putting server config under version control"&#10; title="Server configuration with Git" /&gt;&#10;&lt;h2 id="what-is-it-about"&gt;What is it about?&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;I want version control for the configuration of server applications&lt;/li&gt;&#10;&lt;li&gt;Discussion of technical solutions for implementation&lt;/li&gt;&#10;&lt;li&gt;Tutorial: Exclude Docker/Gitea/other &amp;ldquo;secrets&amp;rdquo; from version control&lt;/li&gt;&#10;&lt;li&gt;Tutorial: Create a deployment pipeline&lt;/li&gt;&#10;&lt;li&gt;introduce automation trigger for subsequent rollout of files on the server&lt;/li&gt;&#10;&lt;li&gt;Next article: Integrate automation on the server and install upgrades&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="background"&gt;Background&lt;/h2&gt;&#10;&lt;p&gt;I have &lt;a href="https://blog.schallbert.de/en/server-auto-upgrade/"&gt;Watchtower running&lt;/a&gt; on &lt;a href="https://blog.schallbert.de/en/projects/move-blog-to-own-server/"&gt;my server&lt;/a&gt; to automatically keep the installed distributions up to date and freshly patched. Recently I had a case where my &lt;a href="https://blog.schallbert.de/en/gitea-action-runner-jekyll-dockerimage/"&gt;Gitea Action Runner&lt;/a&gt; would no longer start automatically and even when started manually it gave an error message.&lt;/p&gt;&#10;&lt;p&gt;Docker had apparently &lt;a href="https://blog.schallbert.de/en/server-auto-upgrade/"&gt;updated itself&lt;/a&gt; without my supervision and was now throwing a volume error when starting up the runner container, which I had never seen before. The error message was clear and could easily be fixed with small changes in a configuration file for Gitea. Nevertheless, I now had the feeling that version control for my configuration would be useful for my future self in order to be able to better understand changes, updates and the reasons behind.&lt;/p&gt;&#10;&lt;h2 id="preliminary-considerations"&gt;Preliminary considerations&lt;/h2&gt;&#10;&lt;p&gt;It sounds like a circular reference to me: I record the configuration files for my server in Gitea, which itself runs on my server. This could become interesting with auto-deployment. But more on that later.&lt;/p&gt;&#10;&lt;p&gt;Two options spontaneously come to mind for getting version control with automatic synchronization:&lt;/p&gt;&#10;&lt;h3 id="1-hardlink"&gt;1. Hardlink&lt;/h3&gt;&#10;&lt;p&gt;This solution would store the configuration files scattered across many folders on my server in a folder declared as a repository using a &lt;a href="https://en.wikipedia.org/wiki/Hard_link" target="_blank" rel="noopener noreferrer" class="external-link"&gt;hardlink&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Why use a hardlink? Because every service on my server runs encapsulated in itself and has its own configuration files and environment variables stored together with the service. Without hardlinks, I would have to version the entire service folder and make my &lt;code&gt;.gitignore&lt;/code&gt; correspondingly complex.&lt;/p&gt;&#10;&lt;p&gt;With Git, I would version the files mapped via hardlinks and make their contents available on Gitea in this way.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&lt;code&gt;schallbert@server:/server-config-files&lt;/code&gt; &amp;ndash;&amp;gt; hardlinks &amp;ndash;&amp;gt; repository &amp;ndash;&amp;gt; Gitea&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;So I would get a downstream &amp;ldquo;version monitoring&amp;rdquo; with a backup copy in the sense that the files are now lying around multiple times.&lt;/p&gt;&#10;&lt;h3 id="2-repo-and-auto-deploy-to-the-server"&gt;2. Repo and auto-deploy to the server&lt;/h3&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-07-15-server-versioncontrol.jpg" alt="Image: Diagram how version control with auto-deploy might work on my server"&gt;&lt;/figure&gt;&#10;&lt;p&gt;In this scenario, I keep the configuration files locally on my laptop and can version them with Git as usual. On Gitea, I would map the repository, and at the end of the chain, my runner would have to auto-deploy on the server every time the configuration changes and then restart the affected containers.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&lt;code&gt;schallbert@laptop:/server-config-files&lt;/code&gt; &amp;ndash;&amp;gt; repository &amp;ndash;&amp;gt; Gitea &amp;ndash;&amp;gt; act-runner &amp;ndash;&amp;gt; &lt;code&gt;server:/&amp;lt;service1...ServiceN&amp;gt;/config-files&lt;/code&gt;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;I would get the administration &amp;ldquo;main&amp;rdquo; on my laptop, and the server would follow the mapping on Gitea.&lt;/p&gt;&#10;&lt;h3 id="problems"&gt;Problems&lt;/h3&gt;&#10;&lt;p&gt;In both cases, I don&amp;rsquo;t have the option of testing changed configurations in advance. Everything I do goes straight to &amp;ldquo;Prod&amp;rdquo; and would be live. In the worst case, I can easily mess up my setup.&lt;/p&gt;&#10;&lt;p&gt;This doesn&amp;rsquo;t change the &lt;em&gt;status quo ante&lt;/em&gt;, where I updated the files directly on the server. So a new problem only in the sense that I didn&amp;rsquo;t have a Prod operation when I set it up in the first place and so there was no risk of failure.&lt;/p&gt;&#10;&lt;p&gt;Solution two seems to be more complex to implement, because I need a deployment pipeline that rolls out the files in the repository on my server. Especially since the runner is in a Docker container, while the configuration files are located directly in the server&amp;rsquo;s file system.&lt;/p&gt;&#10;&lt;p&gt;Direct access to the server file system &lt;a href="https://maze88.dev/docker-socket-from-within-containers.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;is technically possible&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, but according to my limited understanding it would mean &lt;a href="https://dev.to/pbnj/docker-security-best-practices-45ih" target="_blank" rel="noopener noreferrer" class="external-link"&gt;a large attack surface for all my services&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, whose configuration files could now be directly manipulated via the repository in Gitea.&lt;/p&gt;&#10;&lt;h3 id="selecting-my-solution"&gt;Selecting my solution&lt;/h3&gt;&#10;&lt;p&gt;Solution two at least places the immediate live problem on my laptop, so that I don&amp;rsquo;t have to mess around with the production system in the first step. I think it would be easier to set up an integration environment here with which I can check my configuration changes in advance. Since I have all of my services running in Docker, this could perhaps be solved quite easily.&lt;/p&gt;&#10;&lt;h2 id="lets-get-to-work"&gt;Let&amp;rsquo;s get to work&lt;/h2&gt;&#10;&lt;h3 id="create-a-configuration-repo"&gt;Create a configuration repo&lt;/h3&gt;&#10;&lt;p&gt;Okay, then the first step is to get the configuration files from the server. To do this, I use the file transfer command &lt;a href="https://manpages.debian.org/bookworm/openssh-client/scp.1.en.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;scp&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;: &lt;code&gt;scp server:/path/to/source path/to/target&lt;/code&gt; about a dozen times until I have caught all the files.&lt;/p&gt;&#10;&lt;p&gt;I set up the folder structure in this repo exactly as the files are on the server. I hope that this will make my life a little easier later.&lt;/p&gt;&#10;&lt;h3 id="secrets-in-docker-composeyml"&gt;Secrets in &lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;But what do I do with &amp;ldquo;secrets&amp;rdquo; in the configuration files? Private keys, registration tokens, hashes? I would rather not have them lying around more or less openly in the repository. When using &lt;a href="https://docs.docker.com/compose/compose-file/05-services/#env_file" target="_blank" rel="noopener noreferrer" class="external-link"&gt;docker-compose&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; it&amp;rsquo;s quite simple: I can store secret values ​​in hidden files for environment variables and exclude them from Git tracking. In the simplest case, such files are simply called &lt;code&gt;.env&lt;/code&gt; and contain a list of environment variables in the style of&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;BORG_PASSPHRASE&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;lt;redacted&amp;gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In the corresponding &lt;code&gt;docker-compose.yml&lt;/code&gt; I pull the variable from the &lt;code&gt;.env&lt;/code&gt; file as follows:&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;- BORG_PASSPHRASE&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;BORG_PASSPHRASE&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I make these changes locally on my laptop. I use &lt;code&gt;.gitignore&lt;/code&gt; to specify using &lt;code&gt;.*&lt;/code&gt; so hidden files and thus &lt;code&gt;.env&lt;/code&gt; should not be included in the repository. But how do I know whether the containers are still booting correctly?&lt;/p&gt;&#10;&lt;h3 id="secrets-in-giteas-appini"&gt;Secrets in Gitea&amp;rsquo;s &lt;code&gt;app.ini&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;With Gitea, I&amp;rsquo;ve had a much harder time storing secrets in files. The &lt;code&gt;app.ini&lt;/code&gt; is also written dynamically by Gitea, so the file looks a little different every time the service is restarted. After a long search, I found&#10;&lt;a href="https://github.com/go-gitea/gitea/issues/25034" target="_blank" rel="noopener noreferrer" class="external-link"&gt;this issue&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, in which a solution for storing secrets separately was sought and found.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;The only thing that seems sensible to me is storing the values ​​&lt;code&gt;INTERNAL_TOKEN&lt;/code&gt; and &lt;code&gt;SECRET_KEY&lt;/code&gt; separately.&lt;/li&gt;&#10;&lt;li&gt;The other two &lt;a href="https://docs.gitea.com/next/administration/config-cheat-sheet/#server-server" target="_blank" rel="noopener noreferrer" class="external-link"&gt;properties&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; &lt;code&gt;LFS_JWT_SECRET&lt;/code&gt; and &lt;code&gt;JWT_SECRET&lt;/code&gt; are automatically generated anyway and regularly overwritten.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;In my configuration I only use &lt;code&gt;INTERNAL_TOKEN&lt;/code&gt;. So I will copy it in plain text and without quotes into a hidden file (&lt;code&gt;.INTERNAL_TOKEN&lt;/code&gt;) and make it available to the container via a Docker volume:&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&amp;#39;s 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;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;./.INTERNAL_TOKEN:/run/secrets/INTERNAL_TOKEN:ro&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In the &lt;code&gt;app.ini&lt;/code&gt; it is now important to use the path specified in the compose file:&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&amp;#39;s 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:#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;server&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;INTERNAL_TOKEN = /run/secrets/INTERNAL_TOKEN&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Not &lt;code&gt;INTERNAL_TOKEN_URI=/run/secrets/INTERNAL_TOKEN&lt;/code&gt; as stated in the issue linked above, because this creates an error in &lt;code&gt;V1.22.1&lt;/code&gt; I am currently using: &lt;code&gt;Unsupported URI-Scheme&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="a-very-rough-test"&gt;A very rough test&lt;/h3&gt;&#10;&lt;p&gt;To check whether the changed configuration files still work, I install &lt;code&gt;docker&lt;/code&gt; and &lt;code&gt;docker-compose&lt;/code&gt; on my laptop. Then I try to start the containers.&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;# docker console log&#10;Error: Network &amp;#39;caddy-proxy&amp;#39; declared as external, but could not be found.&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Oh right, Docker is not yet configured here. So create the network: &lt;code&gt;sudo docker network create caddy-proxy&lt;/code&gt; and try again. The download of Gitea and its dependencies begins and the container starts - although not as I had imagined: The folder permissions within Docker are incorrect, meaning that neither Gitea nor Act-runner can access all the required files.&lt;/p&gt;&#10;&lt;p&gt;Nevertheless, I find the first error:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;# gitea container log&#10;WARNING: The GITEA_RUNNER_REGISTRATION_TOKEN variable is not set. Defaulting to a blank string.&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I had forgotten to put the token string in quotation marks.&lt;/p&gt;&#10;&lt;p&gt;So for a fully functional integration environment, I have to solve at least two more problems:&#10;Folder permissions for the &lt;code&gt;Main&lt;/code&gt; on my laptop must be set up in such a way that the &lt;code&gt;Docker&lt;/code&gt; user also has write permissions. A simple solution for now is to append a &lt;code&gt;:Z&lt;/code&gt; to the volumes in question and mark them as &lt;a href="https://docs.docker.com/reference/cli/docker/container/run/#volumes-from" target="_blank" rel="noopener noreferrer" class="external-link"&gt;private unshared&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&#10;Now the volume definition in &lt;code&gt;docker-compose.yml&lt;/code&gt; looks like this:&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;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:Z&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I need a second file for environment variables to redirect my services to &lt;code&gt;localhost&lt;/code&gt;. At least the service starts this way and I can see the log output. I can already spot most of the configuration errors.&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;m not sure, but I might have additional problems with the &lt;code&gt;caddyserver&lt;/code&gt; such as certificate management, proxy settings and so on.&lt;/p&gt;&#10;&lt;h2 id="create-a-deploy-pipeline"&gt;Create a deploy pipeline&lt;/h2&gt;&#10;&lt;p&gt;Now it would be great if the files uploaded to the Gitea repo (and previously tested locally for functionality) would automatically find their way to my server. For this I could create another Docker volume where &lt;code&gt;act-runner&lt;/code&gt; would then put the data stored via &lt;code&gt;on:push&lt;/code&gt; trigger. They would then be available on my server.&lt;/p&gt;&#10;&lt;h3 id="setup"&gt;Setup&lt;/h3&gt;&#10;&lt;p&gt;If we remember &lt;a href="https://blog.schallbert.de/en/gitea-action-runner-jekyll-dockerimage/#action-volumes"&gt;my last attempts&lt;/a&gt; to provide artifacts on the server using &lt;code&gt;act_runner&lt;/code&gt;, we can use a large part of that for this task as well:&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;# deploy-to-server.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;# Workflow for saving the server&amp;#39;s config repo to the local disk system&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;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Upload-server-config&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;run-name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;${{ gitea.actor }} uploads server configuration files&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;on&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;push&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;branches&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;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:#f92672"&gt;jobs&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;# Deploy job&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;build&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;runs-on&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ubuntu-latest&lt;/span&gt; &lt;span style="color:#75715e"&gt;# this is the &amp;#34;label&amp;#34; the runner will use and map to docker target OS&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&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:#75715e"&gt;# left: where the output will end up on disk, right: volume name inside container&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;/opt/server-config:/workspace/schallbert/server-config/tmp&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;steps&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;CHECKOUT ---&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;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/checkout@v3&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;with&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;path&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;./tmp&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;RUN FILE CHANGE TRIGGER ---&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;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;&#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; cd ./tmp/automation-hooks-trigger&#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; touch server-config-update.txt&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="strange-volume-errors"&gt;Strange volume errors&lt;/h3&gt;&#10;&lt;p&gt;But the road to this point was rocky. For a long time I had only specified &lt;code&gt;/server-config&lt;/code&gt; under &lt;code&gt;volumes:&lt;/code&gt; on the container side and not the working directory of the runner. Then the action runs through and all commands in the &lt;code&gt;#TEST&lt;/code&gt; section also work. But when I look on my server, the &lt;code&gt;server-config&lt;/code&gt; folder created by Docker remains empty.&lt;/p&gt;&#10;&lt;p&gt;I use the following debug code under &lt;code&gt;RUN FILE CHANGE TRIGGER&lt;/code&gt; to help me find the path errors:&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;# deploy-to-server.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;run&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;&lt;span style="color:#ae81ff"&gt;echo &amp;#34;hello world&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;touch updated.txt&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;pwd&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;ls -al&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I use &lt;code&gt;echo&lt;/code&gt; to check whether my code is even being executed in the runner. The &lt;code&gt;touch&lt;/code&gt; command puts the current timestamp in the &lt;code&gt;updated.txt&lt;/code&gt; file so that I can later use this as a &amp;ldquo;hook&amp;rdquo; for further automation. &lt;code&gt;pwd&lt;/code&gt; shows me the active path within the runner so that I can correctly map the Docker volume to the server hard drive. &lt;code&gt;ls -al&lt;/code&gt; shows me whether the configuration files compiled in the &lt;code&gt;CHECKOUT&lt;/code&gt; step were written correctly.&lt;/p&gt;&#10;&lt;p&gt;This tells me that the volume path on the &amp;ldquo;right side&amp;rdquo; was wrong. I redirect it to the active directory of the runner:&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;# deploy-to-server.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:#75715e"&gt;# left: where the output will end up on disk, right: volume name inside container&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;/opt/server-config:/workspace/schallbert/server-config/&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then I got the following to read:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;# gitea / act-runner console log&#10;failed to create container: &amp;#39;Error response from daemon: Duplicate mount point: /workspace/schallbert/server-config&amp;#39;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It seems that the runner automatically creates the &amp;ldquo;right side&amp;rdquo; of the mount point itself and therefore cannot be reassigned. Only by adding another path part, in my case &lt;code&gt;/tmp&lt;/code&gt; - see &lt;a href="https://blog.schallbert.de/en/server-config-version-control/#setup"&gt;above&lt;/a&gt; - I fix the error and the long-awaited folder &lt;code&gt;server-config&lt;/code&gt; finally appears on my server 😌 with the following content:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;schallbert@schallbert-ubuntu-:/opt/server-config# ls -al&#10;total 52&#10;drwxr-xr-x 9 root root 4096 Jul 12 15:38 .&#10;drwxr-xr-x 9 root root 4096 Jul 11 19:56 ..&#10;-rwxr-xr-x 1 root root 395 Jul 11 20:05 boot-after-backup.sh&#10;drwxr-xr-x 3 root root 4096 Jul 11 20:05 borgmatic&#10;drwxr-xr-x 2 root root 4096 Jul 11 20:05 caddy2&#10;drwxr-xr-x 3 root root 4096 Jul 11 20:05 fail2ban&#10;drwxr-xr-x 8 root root 4096 Jul 12 15:38 .git&#10;drwxr-xr-x 3 root root 4096 Jul 11 20:05 .gitea&#10;drwxr-xr-x 4 root root 4096 Jul 11 20:05 gitea&#10;-rw-r--r-- 1 root root 312 Jul 11 20:05 .gitignore&#10;-rw-r--r-- 1 root root 557 Jul 11 20:05 README.md&#10;-rwxr-xr-x 1 root root 371 Jul 11 20:05 shutdown-for-backup.sh&#10;-rw-r--r-- 1 root root 0 Jul 12 15:38 updated.txt&#10;drwxr-xr-x 2 root root 4096 Jul 11 20:05 watchtower&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="distributing-the-configuration-on-the-server"&gt;Distributing the configuration on the server&lt;/h2&gt;&#10;&lt;p&gt;Okay, that&amp;rsquo;s the first step. I now have a properly configured Git repository that shows my server configuration and can be maintained and at least rudimentarily tested from my laptop. I can also use an automatic &lt;code&gt;Action&lt;/code&gt; to store configuration updates on the server and write an update file with a timestamp.&lt;/p&gt;&#10;&lt;p&gt;Now the update has to be received on the server, distributed and the affected programs and services have to be restarted. But we&amp;rsquo;ll look at this in the article &lt;a href="https://blog.schallbert.de/en/server-config-deploy/"&gt;Roll out server configuration&lt;/a&gt;.&lt;/p&gt;&#10;</description></item><item><title>Milling steel</title><link>https://blog.schallbert.de/en/milling-steel/</link><pubDate>Mon, 10 Jun 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/milling-steel/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-06-10-siemens-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: PC towers with CNC-made window"&#10; title="Milling steel" /&gt;&#10;&lt;p&gt;For our retro gaming booth at the &lt;a href="https://blog.schallbert.de/en/makerfaire-ruhr/"&gt;Maker Faire Ruhr 2024&lt;/a&gt; I milled cutouts in PC cases. I would now like to tell the story behind and share suitable parameters for machining steel on &amp;ldquo;soft&amp;rdquo; hobby machines.&lt;/p&gt;&#10;&lt;h3 id="but-be-careful"&gt;But be careful!&lt;/h3&gt;&#10;&lt;p&gt;First of all, I would like to mention that I made some safety-related stupid mistakes that should definitely be avoided in the future:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Never handle rotating machine parts with gloves on!&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;/li&gt;&#10;&lt;li&gt;Coolants and lubricants must be suitable for the intended purpose; risk of smoke and fire!&lt;/li&gt;&#10;&lt;li&gt;Protective and barrier devices must be firmly mounted and must not be bypassed!&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;So if you are dedicated to occupational and operational safety, you should not watch the video provided below.&lt;/p&gt;&#10;&lt;h2 id="the-workpiece"&gt;The workpiece&lt;/h2&gt;&#10;&lt;p&gt;I have to work on steel sheets from PC cases (MIDI towers) with a thickness of &lt;code&gt;0.7mm-0.9mm&lt;/code&gt;. The material is galvanized, cold-rolled steel (&lt;a href="https://de.cosasteel.com/secc-steel/#SECC_Material_Specification" target="_blank" rel="noopener noreferrer" class="external-link"&gt;SECC&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;), i.e. a cheap and relatively easy-to-work type of steel with a low carbon content and a small admixture of other alloying elements.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-06-10-cad-pcwindow-opensea.jpg" alt="Image: PC tower case service flap drawing"&gt;&lt;/figure&gt;&#10;This type of steel is used for sheet metal in automobile construction and many household appliances, especially in &amp;ldquo;white goods&amp;rdquo;, and is also often used in vending machine and switchboard construction. This milling work can therefore be transferred to various other areas of application.&lt;/p&gt;&#10;&lt;p&gt;The dimensions of the housing sides are between &lt;code&gt;35x45xm&lt;/code&gt; and &lt;code&gt;52x50cm&lt;/code&gt;, so they all fit on my machine. With one exception, the side panels lie flat with their visible side and without cutouts, so my vacuum table can easily be used to hold them down.&lt;/p&gt;&#10;&lt;p&gt;The side wall of a housing has gill-like perforations for ventilation and a recess for a handle. For the grille I use cling film to maintain the vacuum pressure, while the loss of vacuum is accepted when milling the handle recess.&lt;/p&gt;&#10;&lt;h3 id="cadcam"&gt;CAD/CAM&lt;/h3&gt;&#10;&lt;p&gt;The cutouts to be made are all geometrically simple and take into account &lt;code&gt;r=1.5*milling radius&lt;/code&gt; for curves. The window for the &amp;ldquo;sea PC&amp;rdquo; is designed in two parts and is intended to represent a storm scene at sea.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-06-10-cad-pcwindow-opensea-detail.jpg" alt="Image: CAD detail drawing for open-sea-PC"&gt;&lt;/figure&gt;&#10;The window is &lt;code&gt;0.25mm&lt;/code&gt; smaller all around than the recess, which results in an almost play-free fit and looks very professional.&lt;/p&gt;&#10;&lt;p&gt;In the CAM I set up counter-rotating milling, which is advisable for less rigid machines like mine when machining hard and tough materials. The machine runs a little smoother and quieter this way - but it still shakes and vibrates considerably.&lt;/p&gt;&#10;&lt;p&gt;For the depth of cut, I choose a value that is four hundredths of a millimeter less than the material thickness. This way, I practically only leave a wafer-thin film of paint and corrosion protection, but I don&amp;rsquo;t wear out the adhesion promoter mat.&lt;/p&gt;&#10;&lt;h3 id="milling-parameters"&gt;Milling parameters&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-06-10-cutter.jpg" alt="Image: endmill for cutting steel"&gt;&lt;/figure&gt;&#10;Compared to machining wood, the cutting speeds for steel are significantly lower. While I can work with &lt;code&gt;450m/min&lt;/code&gt; in birch multiplex, various cutting data calculators like &lt;a href="https://www.machiningdoctor.com/de/calculators/speeds-and-feeds-calculator/#f1p5" target="_blank" rel="noopener noreferrer" class="external-link"&gt;this one&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; recommend values ​​between &lt;code&gt;80m/min&lt;/code&gt; and &lt;code&gt;200m/min&lt;/code&gt;. This results in a parameter set like &lt;code&gt;S4250u/min, F255mm/min&lt;/code&gt; for my two-cutter made of micro-grain carbide, for example.&lt;/p&gt;&#10;&lt;p&gt;This cutter has cutting edges bevelled at a &lt;code&gt;45°&lt;/code&gt; angle of &lt;code&gt;0.2mm&lt;/code&gt; in length and a helix of &lt;code&gt;30°&lt;/code&gt;. To increase the service life, it is coated with a &lt;a href="https://en.wikipedia.org/wiki/Titanium_aluminium_nitride" target="_blank" rel="noopener noreferrer" class="external-link"&gt;TiAlN&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; alloy approximately &lt;code&gt;3µm&lt;/code&gt; thick using the vacuum process &lt;a href="https://de.wikipedia.org/wiki/Physikalische_Gasphasenabscheidung" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PVD&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;After consulting the cutter manufacturer, I preselected parameters on my machine, which I then adjusted in test runs and was thus able to achieve a higher depth of cut at a lower chip temperature (no more blue chips). Here are the values ​​that I ultimately used for machining:&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;Milling cutter&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Cutter geometry&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Speed&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Feedrate XY&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Feedrate Z&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Depth per pass&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;6mm, Solid Carbide&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2Flutes 30°, l=12, 45°0.2Fase&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;6000u/min&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;550mm/min&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;250mm/min&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0.8mm&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Update: Milling parameters&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2025-12-04T00:00:00Z"&gt;&#10; 2025-12-04&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; After a few questions in various forums on the subject, I have gained new insights that will significantly change my milling parameters for future projects. Further read: &lt;a href="https://blog.schallbert.de/en/milling-secc/"&gt;milling SECC steel&lt;/a&gt;, &lt;a href="https://blog.schallbert.de/en/engrave-aisi304-stainless-steel/"&gt;Engrave stainless steel&lt;/a&gt;&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;When machining steel, pay attention to beveled cutting edges for the milling cutters&lt;/li&gt;&#10;&lt;li&gt;Increase the feed rate, reduce the depth of cut (especially applies to &amp;ldquo;soft&amp;rdquo; machines)&lt;/li&gt;&#10;&lt;li&gt;Dry milling is less problematic than I thought&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;This &lt;a href="https://www.youtube.com/watch?v=WzT4HJalwAs" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Youtube video by Stefan Gotteswinter&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; explains very clearly the advantage of lower chip thicknesses when using beveled cutting edges. This works best when the depth of cut is not greater than the length of the bevel at the end of the cutting edge.&#10;I am also advised on &lt;a href="https://www.cnczone.nl/viewtopic.php?t=23051" target="_blank" rel="noopener noreferrer" class="external-link"&gt;cnczone&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; to choose a &amp;ldquo;high-speed milling&amp;rdquo; approach.&lt;/p&gt;&#10;&lt;p&gt;For my two-cutter, I adjust the cutting data so that I drastically reduce the depth of cut, but increase the feed and speed:&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;Milling cutter&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Cutter geometry&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Speed ​ ​&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Feed XY&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Feed Z&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Depth per pass&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;6mm two-cutter&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2Flutes 30°, l=12, 45°0.2chamfer&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;10600rpm&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;970mm/min&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;450mm/min&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0.2mm&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;I am really looking forward to testing these values ​​in practice.&lt;/p&gt;&#10;&lt;h2 id="preparing-the-milling-process"&gt;Preparing the milling process&lt;/h2&gt;&#10;&lt;p&gt;As this is my first time working with steel, I had to run a few errands and try things out beforehand.&lt;/p&gt;&#10;&lt;h3 id="cooling-lubricant"&gt;Cooling lubricant&lt;/h3&gt;&#10;&lt;p&gt;Cooling is essential here to protect the milling cutter, says the milling cutter manufacturer. So I got myself a syringe with which I can apply lubricant precisely. But: which product should I choose?&lt;/p&gt;&#10;&lt;p&gt;For just three parts, I don&amp;rsquo;t buy a minimum quantity lubrication or cooling lubricants from industrial supplies. So I try out products available at home:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Spray lubricant WD40: Not suitable for me. Evaporates quickly, smells.&lt;/li&gt;&#10;&lt;li&gt;Sewing machine oil / fine oil: Not suitable. Very liquid (flows into vacuum holes), smokes and stinks.&lt;/li&gt;&#10;&lt;li&gt;Rapeseed oil for frying: Wonderful! Lubricates well, relatively viscous, low odor, hardly any smoke development.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="chip-protection"&gt;Chip protection&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-06-10-chipprotector.jpg" alt="Image: This tube made of acrylic glass protects from flying chips"&gt;&lt;/figure&gt;&#10;The metal chips created during processing are short, hot and sharp-edged. They tend to get caught in clothing and fly so far that even at a distance of more than a meter from the machine, plenty of chips still arrive.&lt;/p&gt;&#10;&lt;p&gt;So I sawed off a segment from an acrylic glass tube with a diameter of &lt;code&gt;20cm&lt;/code&gt;, slotted it and hung it on the Z-axis as a chip protection.&lt;/p&gt;&#10;&lt;p&gt;My mistake here: I relied on the Acrylic&amp;rsquo;s clamping force alone and did not additionally attach the chip protection with screws. Fortunately, it only jammed when the last part was being made.&lt;/p&gt;&#10;&lt;h2 id="milling"&gt;Milling&lt;/h2&gt;&#10;&lt;p&gt;You&amp;rsquo;ll notice immediately that the machine does not have the necessary rigidity to process steel. As soon as the milling cutter plunges into the material, the machine begins to rattle and shake. Nevertheless, it bravely fights its way through the material, while I am always ready with a drop of oil at the milling path.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;iframe&#10; src="https://makertube.net/videos/embed/jbtp1jvVi2GLvJq1GdhmgF"&#10; title="Milling Steel!"&#10; loading="lazy"&#10; allow="autoplay; fullscreen; picture-in-picture"&#10; allowfullscreen&#10; style="width: 100%; height: auto; min-height: 315px; aspect-ratio: 16/9;"&#10; &gt;&lt;/iframe&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Milling Steel!&lt;/span&gt;&lt;a href="https://makertube.net/videos/embed/jbtp1jvVi2GLvJq1GdhmgF" class="attr-link" target="_blank" rel="noopener noreferrer"&gt;↗&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;The spindle has more than enough reserves available even at the low speed. However, when positioning the workpiece, I make sure to work as centrally as possible on the ball screw of the Y-axis. This is where the machine experiences the least torsional forces. The chips look good to my eyes.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-06-10-chips.jpg" alt="Image: steel chip quality"&gt;&lt;/figure&gt;&#10;&lt;p&gt;The noise changes when milling: I have more vibrations in the machine when I mill in the Y direction than when only the X-axis moves. I will look into this phenomenon in more detail later.&lt;/p&gt;&#10;&lt;h2 id="the-result"&gt;The result&lt;/h2&gt;&#10;&lt;p&gt;After machining on the CNC, I can simply cut out the windows with a cutter knife.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2024-06-10-cuttingsteel.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Video: Cutting the cutout with a cutter knife&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;I quickly remove the (minimal) burrs with a file.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-06-10-pcwindow-opensea.jpg" alt="Image: Cutout completed and cleaned"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Then I make windows out of transparent acrylic glass, thickness &lt;code&gt;3mm&lt;/code&gt;, which I provide with a &lt;code&gt;10mm&lt;/code&gt; wide fold at depth of &lt;code&gt;1mm&lt;/code&gt; all around. This takes into account the thickness of the double-sided adhesive tape for fitting and the pane sits flush with the PC case.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-06-10-siemens.jpg" alt="Image: The ready-built PC"&gt;&lt;/figure&gt;&#10;&lt;p&gt;The picture above shows a &lt;code&gt;Celsius 1000 Workstation&lt;/code&gt; case from Siemens, which now looks really classy with a simple window.&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;Gloves can be torn away if they accidentally come into contact with the milling cutter. This can result in serious hand injuries.&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>CNC Tuning #3</title><link>https://blog.schallbert.de/en/cnc-tuning-pathinterpreter/</link><pubDate>Sat, 04 May 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/cnc-tuning-pathinterpreter/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-20-G64RD-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Thumbnail of endmill cutting forces"&#10; title="CNC Tuning #3" /&gt;&#10;&lt;p&gt;This is the final part of my series on optimizing manufacturing time on CNC milling machines. It focuses on the path interpreter of the CNC control software.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/cnc-tuning-gcode/"&gt;&lt;em&gt;Part 1&lt;/em&gt;: Better G-Code&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/cnc-tuning-kinematics/"&gt;&lt;em&gt;Part 2&lt;/em&gt;: Turning up kinematic parameters&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The control software path interpreter is responsible for reading the instructions from &lt;code&gt;G-Code&lt;/code&gt; and translating them into machine movements. For example, if &lt;code&gt;G01 X100 Y200 Z-2.0 F2000 S18000&lt;/code&gt; is to be executed, the interpreter must take into account the kinematic parameters of the machine: which ramp is used to accelerate to the target feed, whether the desired feed is even permissible, and from which point braking must be carried out again in order to reach the target point exactly. The data obtained in this way is finally translated into a step sequence for the three stepper motors, loaded into a buffer memory and finally output to the motor amplifier stages in real time.&lt;/p&gt;&#10;&lt;h2 id="step-3-optimizing-the-path-interpreter"&gt;Step 3: Optimizing the path interpreter&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-05-04-laf-settings.jpg" alt="Image: Look Ahead Feed in the CNC control software"&gt;&lt;/figure&gt;&#10;Many control software providers have programmed integrated milling path filters, which can be configured using the &lt;code&gt;G64&lt;/code&gt; command or via the menu. I&amp;rsquo;ll link a few examples from different manufacturers here, where you can read about specific details:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://www.datron.com/resources/blog/toolpath-filter-achieves-a-perfect-surface-finish/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&amp;ldquo;Toolpath filter&amp;rdquo; von Datron&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://docs.edingcnc.com/settings/trajectory" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&amp;ldquo;Look Ahead Feed&amp;rdquo; von EdingCNC&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="http://wiki.linuxcnc.org/cgi-bin/wiki.pl?TrajectoryControl" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&amp;ldquo;Path Blending&amp;rdquo; von LinuxCNC&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The trick now is to find a configuration that suits both milling job and machine kinematics. It&amp;rsquo;s always about a compromise between production speed and path tolerance. If you want to run at high feed rate, sharp edges are actually rounded, connected line segments are approached within a tolerance band or paths are simplified so that path points outside the acceleration that can be achieved by the machine are not just a pass-by.&lt;/p&gt;&#10;&lt;h3 id="basics-what-is-look-ahead-feed"&gt;Basics: What is Look Ahead Feed?&lt;/h3&gt;&#10;&lt;p&gt;This means that the path interpreter of the control software is already looking at the next operation(s) while executing the current milling operation - the algorithm checks how it must brake and accelerate again ahead of time and how to best interconnect, interleave or filter path segments. I explain the physics behind it &lt;a href="https://blog.schallbert.de/en/portal-milling-setup/#acceleration"&gt;in the CNC setup article&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;This can save a lot of time, the milling process becomes more fluid and the machine experiences less stress. In addition to avoiding acceleration and braking processes, parameter &lt;code&gt;G64&lt;/code&gt; can be used to set whether and how milling path filters are allowed to change the planned trajectory in order to further optimize for higher path speeds.&lt;/p&gt;&#10;&lt;h3 id="ideal-for-engraving-operations"&gt;Ideal for engraving operations&lt;/h3&gt;&#10;&lt;p&gt;Such options can be used excellently in engraving operations: higher tolerances are usually OK here, and slightly rounded edges are often acceptable. With 3D engravings, milling through the many paths takes a long time, and a lot of time can be saved here.&lt;/p&gt;&#10;&lt;p&gt;Since I use EdingCNC, I can only give tips for parameters of this control software. Now let&amp;rsquo;s go into detail about the settings!&lt;/p&gt;&#10;&lt;h3 id="milling-path-tolerance-g64-p"&gt;Milling path tolerance &lt;code&gt;G64 P*&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-05-04-lafblending.jpg" alt="Image: Look Ahead Feed G64 CNC corner blending"&gt;&lt;/figure&gt;&#10;This parameter specifies the general tolerance of path accuracy to the control software.&lt;/p&gt;&#10;&lt;p&gt;I have set my machine so that it allows deviations from the specified milling path of up to &lt;code&gt;LAF blending tolerance = 0.3mm&lt;/code&gt; in engraving mode. Alternatively, the configuration can be done using command &lt;code&gt;G64 P0.3&lt;/code&gt;. In the picture I have sketched how such a setting can work in practice: The milling path (white) consists of &lt;code&gt;G01&lt;/code&gt; line segments. The tolerance is marked with the help of circles. The path interpreter of the control software can now independently define circle segments so that the highest possible path speed is achieved within the tolerance.&lt;/p&gt;&#10;&lt;p&gt;The path determined in this way (turquoise) resembles a spline curve. It should be noted that all lines can be affected by this setting - even those that have strict tolerance requirements. If this is not desired, a smaller value should be used here.&lt;/p&gt;&#10;&lt;h3 id="path-rounding-g64-r"&gt;Path rounding &lt;code&gt;G64 R*&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;If two line segments meet at an angle of up to &lt;code&gt;LAF full speed blending angle threshold = 12°&lt;/code&gt;, the machine will try to follow the angle exactly without braking. For larger angles, travel is carried out at reduced speed with a tolerance defined by &lt;code&gt;G64 P*&lt;/code&gt;. This configuration is programmed with &lt;code&gt;G64 R12&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;The values ​​here must be set with great caution because they lead to acceleration peaks on the machine. Large values ​​can cause step losses and increased wear on the CNC milling machine. To achieve this, the path speed and accuracy are maintained optimally.&lt;/p&gt;&#10;&lt;h3 id="path-rounding-at-reduced-feed-g64-s-d"&gt;Path rounding at reduced feed &lt;code&gt;G64 S* D*&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-05-04-G64RD.jpg" alt="Image: Look Ahead Feed G64 CNC corner radius"&gt;&lt;/figure&gt;&#10;This configuration gives the control software the ability to round edges up to the set tolerance value: &lt;code&gt;LAF reduced speed path smoothing delta = 0.001mm&lt;/code&gt;. In combination with the rounding, the angle up to which this rounding should be carried out must be set: &lt;code&gt;LAF reduced speed path smoothing angle threshold = 0°&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;This function is best programmed in combination with path rounding at full feed like so: &lt;code&gt;G64 R* S* D*&lt;/code&gt;. The advantage here is that line segments are followed very precisely as shown in the picture, and at the same time the edges are rounded off in a finely adjustable way. Up to the angle defined via &lt;code&gt;R&lt;/code&gt;, the path is followed exactly, even at full feed.&lt;/p&gt;&#10;&lt;p&gt;If very large angles are set at &lt;code&gt;S&lt;/code&gt;, there will be greater rounding corresponding to &lt;code&gt;D&lt;/code&gt;. However, if the angle specified in the milling program exceeds the configured value, the algorithm falls back to the values ​​stored in &lt;code&gt;P&lt;/code&gt; or to the exact path following mode.&lt;/p&gt;&#10;&lt;h3 id="path-simplification-g64-q"&gt;Path simplification &lt;code&gt;G64 Q*&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-05-04-lafcombine.jpg" alt="Image: Look Ahead Feed G64, path combine setting"&gt;&lt;/figure&gt;&#10;If the G-code file contains numerous, very short line segments (as is often the case when creating paths from image files), this function can help by simplifying the milling paths. The number behind the parameter indicates the tolerance up to which line segments are merged into a line.&lt;/p&gt;&#10;&lt;p&gt;This setting relieves the load on the machine&amp;rsquo;s buffer memory and at the same time increases the average path feed rate. I think this setting is particularly interesting for engraving operations when working with &amp;ldquo;noisy&amp;rdquo; data and there was no option in CAD to reduce the number of paths.&lt;/p&gt;&#10;&lt;h3 id="acceleration-filter-g64-f"&gt;Acceleration filter &lt;code&gt;G64 F*&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;Using these parameters, the acceleration peaks that may be generated by Look Ahead Feed can be mitigated. The minimum value to be set is determined by the quotient of the selected feed for the operation and the maximum speed of the machine.&lt;/p&gt;&#10;&lt;p&gt;Example: &lt;code&gt;Fmin = Foperate / Fg00&lt;/code&gt;, with &lt;code&gt;Fg00 = 12000mm/min&lt;/code&gt; and &lt;code&gt;Foperate = 5000mmm/min&lt;/code&gt;, &lt;code&gt;Fmin = 0.42&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;So at a feed rate of &lt;code&gt;5000mm/min&lt;/code&gt; the maximum possible acceleration of the machine is called up and the curve radii are set so that they are ran at a constant path speed. All values ​​greater than or equal to &lt;code&gt;Fmin = 1&lt;/code&gt; are inherently safe, regardless of the selected feed rate.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-05-04-accelfilter.jpg" alt="Image: acceleration filter for G64 operation"&gt;&lt;/figure&gt;&#10;This parameter was introduced for better processing of elastic materials such as rubber or cork, where a constant feed speed is critical and path stability is of secondary importance. The parameter is used in combination with &lt;code&gt;G64 R*&lt;/code&gt;. The following programming for the example above would call up the full capacity of the machine: &lt;code&gt;G64 R120 F0.42&lt;/code&gt;, whereby &lt;code&gt;F5000&lt;/code&gt; is used, &lt;code&gt;F12000&lt;/code&gt; is the maximum feed of the machine and acute angles up to &lt;code&gt;120°&lt;/code&gt; are taken without braking at all.&lt;/p&gt;&#10;&lt;p&gt;If I apply these values ​​to my machine, the image shown opposite is created. A rectangle with an edge length of &lt;code&gt;25 x 100mm&lt;/code&gt; should be made here. The target trajectory is again shown in white. The turquoise curve shows the curve with a constant path speed that was driven by programming the acceleration filter. The lower the feed in relation to the maximum feed and the higher the acceleration of the machine is set, the smaller the necessary radius becomes.&lt;/p&gt;&#10;&lt;p&gt;However, in my experience, this parameter is not used for engraving and milling solid materials.&lt;/p&gt;&#10;&lt;h2 id="my-configuration-for-the-hobbyline-4530"&gt;My configuration for the Hobbyline 4530&lt;/h2&gt;&#10;&lt;p&gt;I played around with the various LAF settings at the cost of a lot of blanks to learn and improve parameter settings. I achieved the shortest production times by previously optimizing the kinematic parameters with the following configuration:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&lt;code&gt;G64 Q0.3 R12 D0.3 S120&lt;/code&gt;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;With that I tell the machine the following:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Use the look-ahead feed algorithm and try to merge short line segments less than &lt;code&gt;0.3mm&lt;/code&gt; in length. Try not to reduce feed rate when two line segments are at an angle of less than &lt;code&gt;12°&lt;/code&gt;. At larger angles up to &lt;code&gt;120°&lt;/code&gt;, rounding can be done with up to &lt;code&gt;r=0.3mm&lt;/code&gt; tolerance. For even larger angles, the specified path must be followed. If necessary, brake to a standstill.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-05-04-duplicated-software.jpg" alt="Image: dual install of CNC control software for different purposes"&gt;&lt;/figure&gt;&#10;Since the parameter optimization for engraving operation took me quite far away from the parameter set for normal milling operation, I simply installed my control program a second time.&lt;/p&gt;&#10;&lt;p&gt;There I was then able to specify the speed, axis acceleration and path parameterization separately. Now I have the best of both worlds on one machine:&lt;/p&gt;&#10;&lt;p&gt;A very precise, smooth milling operation with the following parameters:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;Fmax XY 12000mm/min&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Fmax Z 4800mm/min&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Amax X 1000mm/s²&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Amax Y 1000mm/s²&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Amax Z 1000mm/s²&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;G64 P0.1 R6&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;And via the other link an extremely fast engraving operation:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;Fmax XY 12000mm/min&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Fmax Z 4800mm/min&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Amax X 4000mm/s²&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Amax Y 3200mm/s²&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Amax Z 1600mm/s²&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;G64 Q0.3 R12 D0.3 S120&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="and-where-is-the-proof"&gt;And where is the proof?&lt;/h2&gt;&#10;&lt;p&gt;The embedded video shows the same engraving project with four different levels of optimization:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;top left: stock configuration of the machine, standard milling paths from CAD/CAM&lt;/li&gt;&#10;&lt;li&gt;top right: Speed ​​and acceleration increased to &amp;ldquo;milling mode&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;bottom left: Look Ahead feed switched on &lt;code&gt;G64 P0.3&lt;/code&gt;, milling paths optimized in CAM&lt;/li&gt;&#10;&lt;li&gt;bottom right: Optimized for &amp;ldquo;engraving mode&amp;rdquo; as described above, CAM flyover reduced to &lt;code&gt;2mm&lt;/code&gt; and empty runs minimized&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;iframe&#10; src="https://makertube.net/videos/embed/2EWe1ZvVW9TKY6k3XtpmWF"&#10; title="Tuned Engraving: Compare"&#10; loading="lazy"&#10; allow="autoplay; fullscreen; picture-in-picture"&#10; allowfullscreen&#10; style="width: 100%; height: auto; min-height: 315px; aspect-ratio: 16/9;"&#10; &gt;&lt;/iframe&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Tuned Engraving: Compare&lt;/span&gt;&lt;a href="https://makertube.net/videos/embed/2EWe1ZvVW9TKY6k3XtpmWF" class="attr-link" target="_blank" rel="noopener noreferrer"&gt;↗&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;</description></item><item><title>CNC Tuning #2</title><link>https://blog.schallbert.de/en/cnc-tuning-kinematics/</link><pubDate>Sat, 27 Apr 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/cnc-tuning-kinematics/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-27-kinematics-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Thumbnail of a CNC machine&amp;#39;&amp;#39;s portal X-Axis"&#10; title="CNC Tuning #2" /&gt;&#10;&lt;p&gt;This is the second part of my series on optimising production time on CNC milling machines. It focuses on the kinematic machine parameters for engraving operations.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/cnc-tuning-gcode/"&gt;&lt;em&gt;Part 1&lt;/em&gt;: Better G-code&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/cnc-tuning-pathinterpreter/"&gt;&lt;em&gt;Part 3:&lt;/em&gt; Tuning path interpreter&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="step-2-tuning-the-kinematic-parameters-of-the-portal-milling-machine"&gt;Step 2: Tuning the kinematic parameters of the portal milling machine&lt;/h2&gt;&#10;&lt;p&gt;I have explained the theoretical basics in detail in the &lt;a href="https://blog.schallbert.de/en/portal-milling-setup/"&gt;article on CNC control configuration&lt;/a&gt;. After many tests and consultation with the manufacturer, I recorded my problems with it and the associated solutions in &lt;a href="https://blog.schallbert.de/en/projects/one-year-zerspanobert/#velocityerror"&gt;Conclusion on one year of CNC operation&lt;/a&gt;. The following sections apply the above findings to a demonstration machine that I was able to borrow.&lt;/p&gt;&#10;&lt;h3 id="travelling-speed-in-rapid-traverse"&gt;Travelling speed in rapid traverse&lt;/h3&gt;&#10;&lt;p&gt;For the demo machine, I was able to increase the rapid traverse speed from the suggested &lt;code&gt;Fxy = 4m/min&lt;/code&gt; and &lt;code&gt;Fz = 2.4m/min&lt;/code&gt; to &lt;code&gt;Fxy = 12m/min&lt;/code&gt; and &lt;code&gt;Fz = 4.8m/min&lt;/code&gt;. If I set all three axes in motion simultaneously, I get a combined step frequency of &lt;code&gt;96kHz&lt;/code&gt; due to the pitch of the ball screws and the microstep configuration (8 per full step) of the motors, which is below the maximum step frequency of &lt;code&gt;125kHz&lt;/code&gt; of the controller.&lt;/p&gt;&#10;&lt;h3 id="acceleration-in-rapid-traverse"&gt;Acceleration in rapid traverse&lt;/h3&gt;&#10;&lt;p&gt;The manufacturer&amp;rsquo;s specifications recommend &lt;code&gt;aXY = 300mm/s²&lt;/code&gt; and &lt;code&gt;aZ = 200mm/s²&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;I increased the acceleration in several steps and ended up with &lt;code&gt;aXY = 1000mm/s²&lt;/code&gt; and &lt;code&gt;aZ = 400mm/s²&lt;/code&gt; in a first round. In my experience, these values are easily possible for this small and light machine with a travel of only &lt;code&gt;30x45x14cm&lt;/code&gt; for normal operation. They greatly increase the production speed, because every change of direction, including those in the material grip at &lt;code&gt;G01, G02, G03&lt;/code&gt;, are now carried out faster by a factor of &lt;code&gt;3&lt;/code&gt; / &lt;code&gt;2.5&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;I have often operated the machine this way and I have also driven stably and safely with these values at the &lt;a href="https://blog.schallbert.de/en/makerfaire-ruhr/"&gt;maker faire&lt;/a&gt;. The following video shows the milling machine engraving the letter &amp;ldquo;e&amp;rdquo; with values specified above.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;iframe&#10; src="https://makertube.net/videos/embed/vvmwT8e6vWnj5zbuqMdUfN"&#10; title="TunedEngraving: Teaser"&#10; loading="lazy"&#10; allow="autoplay; fullscreen; picture-in-picture"&#10; allowfullscreen&#10; style="width: 100%; height: auto; min-height: 315px; aspect-ratio: 16/9;"&#10; &gt;&lt;/iframe&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;TunedEngraving: Teaser&lt;/span&gt;&lt;a href="https://makertube.net/videos/embed/vvmwT8e6vWnj5zbuqMdUfN" class="attr-link" target="_blank" rel="noopener noreferrer"&gt;↗&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;After that, however, I wanted to know how far I could take it. True to the motto described above&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;If you don&amp;rsquo;t cross the line, you don&amp;rsquo;t know where it is&amp;rdquo;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;I first tested what massive step losses sound like on this machine. So I blocked one axis with a wooden beam and carefully ran into it:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2024-04-27-soundoflosingsteps.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Massive step loss soundcheck&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;The clearly audible chattering occurs when the motor starts up, but several full steps are lost at target speed. If the rotor is already &amp;ldquo;lost&amp;rdquo; when the motor starts up, the motor only chirps without setting the axis in motion at all. If the motor only loses a few steps, but is at target speed, there is a rough and scratchy sound.&lt;/p&gt;&#10;&lt;p&gt;I chose the X-axis for the experiment because it is the easiest to lock. I also blocked it in the &lt;code&gt;X+&lt;/code&gt; direction. Reason: this machine does not have a professional fixed bearing to absorb the compressive and tensile forces of the ball screw. Instead, compressive forces in the direction of the stepper motor are safely absorbed by a flange mounted on the spindle, which presses against ball bearings located in the portal cheek.&lt;/p&gt;&#10;&lt;p&gt;Tensile forces, on the other hand, are mainly absorbed by the motor bearings - I didn&amp;rsquo;t want to take any risks here.&#10;So now the noise caused by sudden and jerky step loss is known - overclocking can begin!&lt;/p&gt;&#10;&lt;p&gt;My approach:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;double the acceleration of one axis of the machine&lt;/li&gt;&#10;&lt;li&gt;jog around the axis in question, listening for strange noises and observing the axis movement&lt;/li&gt;&#10;&lt;li&gt;repeat the previous steps until massive step losses occur&lt;/li&gt;&#10;&lt;li&gt;reduce acceleration to the average value between &amp;ldquo;works&amp;rdquo; and &amp;ldquo;does not work&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;repeat the previous step until the machine runs smoothly&lt;/li&gt;&#10;&lt;li&gt;perform all steps for the other axes as well&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;This is how I ended up with &lt;code&gt;aXY = 4000mm/s²&lt;/code&gt;, &lt;code&gt;aZ = 1600mm/s²&lt;/code&gt;, which is unbelievable for such a hobby machine. This is now an order of magnitude higher than the recommended values.&lt;/p&gt;&#10;&lt;p&gt;The small milling machine also seems to run stably here and the engravings produced look good. However, I could not imagine that I was still operating the stepper motors, driver stages and power supply unit within their specifications. That&amp;rsquo;s why I took measurements.&lt;/p&gt;&#10;&lt;h3 id="electrical-measurements"&gt;Electrical measurements&lt;/h3&gt;&#10;&lt;p&gt;The power supply unit is specified with &lt;code&gt;3.7A&lt;/code&gt; at &lt;code&gt;36V&lt;/code&gt;. I must therefore not exceed this value permanently, neither in rapid traverse &lt;code&gt;G00&lt;/code&gt; nor in material engagement. As explained above, the rapid traverse in engraving mode is decisive due to the lower milling forces, so I have to pay particular attention to the simultaneous movement of several axes.&lt;/p&gt;&#10;&lt;p&gt;The motors can each handle a continuous current of &lt;code&gt;3.0A&lt;/code&gt;, so they don&amp;rsquo;t limit me here. In the electrical tests, I only check whether the energy consumption of the system remains within limits.&lt;/p&gt;&#10;&lt;h4 id="test-setup"&gt;Test setup&lt;/h4&gt;&#10;&lt;p&gt;I connect the multimeter known from &lt;a href="https://blog.schallbert.de/en/measure-inrush-current/"&gt;this article&lt;/a&gt; directly to the output of the power supply unit and record voltage and current. For the experiment, I simulate the production of the engraving linked in the video above and in two runs first carry out a temporally coarse-resolution power factor analysis (&lt;code&gt;2Hz&lt;/code&gt;), followed by a fine-resolution current determination with &lt;code&gt;20kHz&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h4 id="results"&gt;Results&lt;/h4&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-27-powerdiagram-engrave.jpg" alt="Image: Power diagram for tuned engraving"&gt;&lt;/figure&gt;&#10;&lt;p&gt;The figure above shows voltage, current, effective and reactive power. It can be seen that the reactive power (&lt;code&gt;green&lt;/code&gt;) always shows short peaks when several axles accelerate into rapid traverse at the same time. The power consumption of an axis in &lt;code&gt;G01/G02/G03, blue&lt;/code&gt; is on average around &lt;code&gt;45W&lt;/code&gt;, similar to that of constant speed in rapid traverse.&lt;/p&gt;&#10;&lt;p&gt;The power supply unit is able to keep the voltage &lt;code&gt;orange&lt;/code&gt; fairly constant and even under full load only drops by just under &lt;code&gt;0.2V&lt;/code&gt;. Only the current value looked a little too smooth to me. At this point I suspected that the time resolution was too low and decided to repeat the experiment focussing on the current.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-27-currentdiagram-engrave.jpg" alt="Image: Current diagram for tuned engraving"&gt;&lt;/figure&gt;&#10;&lt;p&gt;And indeed. The higher temporal resolution shows that the current consumption during acceleration of several axes shows peaks that are higher than the values already known.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-27-currentpeak-engrave.jpg" alt="Image: Zoomed-in current draw for axis acceleration"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Movements on the Y-axis, which must set the entire portal in motion, are creating current spikes, especially when the other axes are still moving or have just stopped. This is related to the stepper motor drivers. They continue to brake the motors at full power for about &lt;code&gt;0.5s&lt;/code&gt; after stopping. The resulting overlay makes the tips look even more dramatic.&lt;/p&gt;&#10;&lt;h4 id="peak-load-benchmark"&gt;Peak load benchmark&lt;/h4&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-27-loadtest.jpg" alt="Image: Zoomed-in current draw for axis acceleration"&gt;&lt;/figure&gt;&#10;Well then let&amp;rsquo;s take it to the extreme with an acceleration benchmark. Will the power supply be able to maintain the output voltage even under full load while accelerating all motors at the same time?&lt;/p&gt;&#10;&lt;p&gt;To test this, I wrote G-code that makes all axes simultaneously accelerate to maximum speed and then decelerate to zero again. Then it will return to the starting point under the same conditions and repeat the whole thing.&lt;/p&gt;&#10;&lt;p&gt;In this way I can find out whether the power supply has enough reserves to handle load peaks and at the same time see whether the motors are already losing steps to a large extent. On average, the applied power is &lt;code&gt;95W&lt;/code&gt; and the supply voltage drops by &lt;code&gt;0.28V&lt;/code&gt; to &lt;code&gt;35.72V&lt;/code&gt;. The power supply must deliver a peak of &lt;code&gt;5.14A&lt;/code&gt;, which is almost &lt;code&gt;40%&lt;/code&gt; above the rated current.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2024-04-20-benchmark.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Benchmark&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;If you listen closely, the machine sounds a bit rough right at the start of the benchmark, but then it settles down. Lost steps can sound like this too. It is therefore very important to monitor the machine with all your senses.&lt;/p&gt;&#10;&lt;h3 id="mechanical-testing"&gt;Mechanical testing&lt;/h3&gt;&#10;&lt;p&gt;Now for the final test: During the rapid traverse benchmark, I put additional load on the axes by pulling on the respective axis with my arm while the program is running. The Z and X axes continue to run stably. However, with the portal axis Y I notice that the machine now sounds different. Somehow rough, as if you were lightly dragging a rasp over a wooden board.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-27-steplossy.jpg" alt="Image: Y-axis reveals massive step loss"&gt;&lt;/figure&gt;&#10;&lt;p&gt;If I apply a large load, the stepper motor on this axis no longer even starts, but just chirps. It can no longer follow the pulses of the power amplifier. So I now reduce the acceleration step by step until the motor can accelerate the portal axis repeatably and without audible step losses, even under medium load. Here I end up with a still fast &lt;code&gt;[aX = 4000mm/s² aY = 3200mm/s² aZ = 1600mm/s²]&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;In a final step, after repeating the benchmark five times, I carry out another reference run and look at the control software logs. All axes are now unobtrusive in terms of step losses.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-27-steplossxz.jpg" alt="Image: XZ-axis without step loss"&gt;&lt;/figure&gt;&#10;&lt;h3 id="appeals-for-cnc-tuning"&gt;Appeals for CNC tuning&lt;/h3&gt;&#10;&lt;p&gt;Please be careful when tuning your machine. Get it to know properly before you start. Listen carefully and optimize axis by axis one after the other. Take time for tests, verifications and experiments.&lt;/p&gt;&#10;&lt;p&gt;Even if you have a portal milling machine that is very similar to mine, other dimensions make a huge difference in terms of stiffness and therefore possible travel speed and acceleration. Even the state of maintenance, smoother shafts, different guides, etc. can mean that you end up running different values ​​than me.&lt;/p&gt;&#10;&lt;p&gt;No professional equipment is required to explore the limits of the machine. Not even a dial gauge, since step losses can also be determined with sufficient precision using the reference switches.&lt;/p&gt;&#10;&lt;p&gt;All I found from the electrical measurements above is that the manufacturer did a good job designing the machine and no component is undersized. On the contrary, this machine is ideal for overclocking.&lt;/p&gt;&#10;&lt;p&gt;And now on to the last &lt;a href="https://blog.schallbert.de/en/cnc-tuning-pathinterpreter/"&gt;&lt;em&gt;Part 3:&lt;/em&gt; Optimize the path interpreter&lt;/a&gt;.&lt;/p&gt;&#10;</description></item><item><title>CNC Tuning #1</title><link>https://blog.schallbert.de/en/cnc-tuning-gcode/</link><pubDate>Sat, 20 Apr 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/cnc-tuning-gcode/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-20-camtuned-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Thumbnail of CAM simulation for G-code optimizing"&#10; title="CNC Tuning #1" /&gt;&#10;&lt;p&gt;This is the first part of my series on optimising production time on CNC milling machines. It focuses on the use of CAD and CAM tools to generate more efficient G-code.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/cnc-tuning-kinematics/"&gt;&lt;em&gt;Part 2&lt;/em&gt;: Turning up kinematic parameters&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/cnc-tuning-pathinterpreter/"&gt;&lt;em&gt;Part 3:&lt;/em&gt; Optimise path interpreter&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;There are various measures to reduce production time with a CNC machine. Firstly, however, it should be analysed why this is desired and what possible disadvantages result from it.&lt;/p&gt;&#10;&lt;h2 id="why-at-all"&gt;Why at all?&lt;/h2&gt;&#10;&lt;p&gt;From a commercial point of view, shorter production times ensure a higher throughput of parts and thus enable a higher turnover in a given period of time. In addition, the investment in the machine is amortised more quickly.&lt;/p&gt;&#10;&lt;p&gt;As a hobbyist, shorter production times are particularly interesting if the aim is to produce very complex geometries or when performing real 3D-milling. Here, machine times can easily amount to several hours per part and the potential savings are therefore particularly large.&lt;/p&gt;&#10;&lt;p&gt;For me, as in teaching, the scientific idea takes centre stage: I want to find out where the limit of the machine lies. I want to know why it is there. I will try to operate it stably within its limits. Still, wear and tear must be kept under control.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;However, in order to know this limit precisely, it must first be crossed. How else would I know where it is?&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h3 id="what-are-the-possible-disadvantages"&gt;What are the possible disadvantages?&lt;/h3&gt;&#10;&lt;p&gt;Certain components of an &amp;ldquo;overclocked&amp;rdquo; machine could wear out faster than usual. These include power supply units and motor output stages, for example, which have to cope with higher transient currents. Bearings and guides are subjected to greater stress due to higher accelerations. Rotating spindles and machine frames have to absorb the resulting stronger and more rapidly changing forces and vibrations and may show signs of fatigue or loosening connections earlier.&lt;/p&gt;&#10;&lt;p&gt;In extreme cases, the machine can even be damaged when the limits are reached: There is a high probability that step losses&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; will occur beyond the limit. This is not a problem in test operation. However, the assumed position then no longer matches the actual position and there is a risk of a &amp;ldquo;crash&amp;rdquo; - if care is not taken. Here is a pretty good &lt;a href="https://www.youtube.com/watch?v=C_9dM1wx0Sg" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Youtube video by &lt;em&gt;gammaflow&lt;/em&gt; on the subject of avoiding step losses&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;If the milling paths are changed in CAD or CAM in favour of shorter production times, this can also have negative effects on cut, edge and surface quality.&lt;/p&gt;&#10;&lt;p&gt;A compromise must also be made when optimising the path interpreter of the control software: Higher production speed is bought with lower accuracy and/or higher machine wear.&lt;/p&gt;&#10;&lt;h3 id="which-measures-are-not-discussed"&gt;Which measures are not discussed?&lt;/h3&gt;&#10;&lt;p&gt;I will not consider here the optimisation of the feed rate &lt;code&gt;F&lt;/code&gt; and speed &lt;code&gt;S&lt;/code&gt; of the selected cutter during machining. These parameters depend on the material to be machined, the endmill geometry, the capabilities of the machine and the courage of the operator, so I cannot suggest any generally applicable tunings.&lt;/p&gt;&#10;&lt;p&gt;I must also exclude another major influencing factor here: The machining strategy of the CAM program in relation to rapid traverse. My CAM program does not allow me to intervene very deeply, so I always have to work manually in the G-code to optimise the last few percent of production time, which is correspondingly error-prone.&lt;/p&gt;&#10;&lt;h2 id="tuning-in-three-steps"&gt;Tuning in three steps&lt;/h2&gt;&#10;&lt;p&gt;In my optimisation, I proceeded in three steps, which can be implemented independently of each other. Each step brought me clearly noticeable improvements. This article focuses on engraving operation for several reasons:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Engraving usually allows for greater tolerances than other modes of operation. This allows greater rounding, which leads to lower accelerations, enabling higher feed rates on average&lt;/li&gt;&#10;&lt;li&gt;The milling forces to be expected are lower: No cutting all the way through, hardly any energy required for chip displacement, smaller chip volume than with end mills&lt;/li&gt;&#10;&lt;li&gt;This allows the machine to be operated closer to its mechanical and electrical limits&lt;/li&gt;&#10;&lt;li&gt;More optimisation potential due to larger G-code files on average&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="step-1-better-g-code"&gt;Step 1: Better G-code&lt;/h2&gt;&#10;&lt;p&gt;This section follows the credo &lt;code&gt;Garbage in, garbage out&lt;/code&gt;. In other words: If my G-code is already bad, the machine can never achieve optimum production times. So what characterises &amp;ldquo;bad&amp;rdquo; G-code in terms of production time?&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Excessively complex and long, possibly file sizes in the megabyte range&lt;/li&gt;&#10;&lt;li&gt;Extremely many interpolation points and a lot of short line segments&lt;/li&gt;&#10;&lt;li&gt;A bunch of &amp;ldquo;empty runs&amp;rdquo; in rapid traverse on XY with the Z-axis raised&lt;/li&gt;&#10;&lt;li&gt;Many retractions of the Z-axis to safety height&lt;/li&gt;&#10;&lt;li&gt;Frequent machine waiting times (spindle run-up, coolant pump, controller buffer full)&lt;/li&gt;&#10;&lt;li&gt;Excessive superfinishing even with B surfaces&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="but-how-should-i-simplify-my-g-code"&gt;But how should I simplify my G-code?&lt;/h3&gt;&#10;&lt;p&gt;Short question, long answer: It starts in CAD and may not end in CAM.&lt;/p&gt;&#10;&lt;h3 id="example-cad"&gt;Example CAD&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-20-bline.jpg" alt="Image: Letter &amp;#39;B&amp;#39; in CAM, drawn as a set of line segments"&gt;&lt;/figure&gt;&#10;Let&amp;rsquo;s assume we want to mill some letters. Our CAD program has a bunch of great fonts for this. We now create a DXF file for the CAM. In the CAM, when we zoom in, we suddenly see that all the letters consist of very short line segments instead of circle segments or &lt;a href="https://en.wikipedia.org/wiki/Spline_%28mathematics%29" target="_blank" rel="noopener noreferrer" class="external-link"&gt;splines&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. The result: The CAM will generate &lt;code&gt;G01&lt;/code&gt; movements, i.e. &amp;ldquo;linear moves&amp;rdquo; or a sequence of lines.&lt;/p&gt;&#10;&lt;p&gt;The connected paths require the machine to change direction very often. Consequently, it must always decelerate before the connection point and then accelerate again. The machine cannot therefore travel at a constant speed, as this would require infinite acceleration at the bend point.&lt;/p&gt;&#10;&lt;p&gt;There are solutions to this problem, which are ideally used together:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Set the CAD programme to create splines or curves instead of line segments whenever possible&lt;/li&gt;&#10;&lt;li&gt;Simplify the drawing by reducing the number of support points&lt;/li&gt;&#10;&lt;li&gt;Increased use of radii and splines at edges, avoidance of angles&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;In my case, the first option was not available, so I now use a different programme for engraving fonts than for my usual drawings. As a result, I can now express lettering in line and arc segments. The milling time required for this is halved and at the same time the G-code manages with fewer lines. In addition, the cuts become rounder and adheres even more closely to the initially selected font.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-20-bsplinevsline.jpg" alt="Image: Letter &amp;#39;B&amp;#39; in CAM, twice, once per spline, once as line segments"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Gimp and Inkscape provide good examples of simplifying paths by reducing vertices - I have already shown this for Inkscape in my &lt;a href="https://blog.schallbert.de/en/path-from-image/#inkscape"&gt;article on image-to-path conversion&lt;/a&gt;. With just a few clicks, you can obtain drawing data at the expense of accuracy, some of which can be produced much faster.&lt;/p&gt;&#10;&lt;p&gt;As an end mill cannot produce a pocket with &lt;code&gt;r=0&lt;/code&gt; in the corners anyway, I have got into the habit of rounding off all corners and edges in CAD. Not only do parts produced in this way fit better in the hand, they are also quicker to produce - the time savings add up, especially on slow and sluggish machines.&lt;/p&gt;&#10;&lt;h3 id="example-cam"&gt;Example CAM&lt;/h3&gt;&#10;&lt;p&gt;In CAM, I can save a little time by increasing the use of approach points and ramps. I usually place them on the shortest line segment and thus save one braking/acceleration process. With the plunge type, the advantage of &amp;ldquo;ramp&amp;rdquo; over &amp;ldquo;helix&amp;rdquo; is that all paths of the cutter are effective paths and I can drive through without additional axis accelerations. Depending on the ramp gradient, however, the milling path is longer as overlaps occur.&lt;/p&gt;&#10;&lt;p&gt;The clever selection of clearing infeeds and strategies for pockets to suit the material, milling cutter and desired surface finish has a much greater effect on the production time. In many cases, trochoidal milling&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; produces longer files and takes more time on the machine than the classic &amp;ldquo;lawnmower&amp;rdquo;, which simply files the pocket. For the clearing infeed, I like to choose large values above &amp;lsquo;60%&amp;rsquo; for materials such as wood and acrylic glass - unless the bottom of the pocket has to meet particularly high quality standards.&lt;/p&gt;&#10;&lt;p&gt;In my opinion, it is quickest for engravings not to use a second cutter for pocket clearing. Instead, use an engraving bit with a flattened tip - similar to those for my &lt;a href="https://blog.schallbert.de/en/cut-dibond-tests/"&gt;tests with Dibond&lt;/a&gt;. This takes longer to clear, but I can compensate for this by increasing the engrave depth and a fairly wide flattening of &lt;code&gt;1.5mm&lt;/code&gt;. I use a clearing infeed in XY of &lt;code&gt;1mm&lt;/code&gt; with such a milling cutter. What remains is the time advantage due to fewer tool changes.&lt;/p&gt;&#10;&lt;h3 id="but-how-can-i-reduce-the-empty-runs"&gt;But how can I reduce the empty runs?&lt;/h3&gt;&#10;&lt;p&gt;At last a few crisp, short answers:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Lower retraction height in &lt;code&gt;Z&lt;/code&gt; (I use &lt;code&gt;5mm&lt;/code&gt; but &lt;code&gt;2mm&lt;/code&gt; would probably also work for me)&lt;/li&gt;&#10;&lt;li&gt;Optimise the milling strategy in the CAM to &amp;ldquo;minimum paths&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;For nesting: Make sure that the geometry is not produced line by line, but part by part.&lt;/li&gt;&#10;&lt;li&gt;If in doubt, optimise the G-code by hand. Worthwhile if the CAM program cannot work well enough and many identical parts have to be produced&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="and-where-is-the-problem-with-retractions-of-the-z-axis"&gt;And where is the problem with retractions of the Z-axis?&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Retractions occur very frequently, especially in engraving operations&lt;/li&gt;&#10;&lt;li&gt;the loss of time adds up&lt;/li&gt;&#10;&lt;li&gt;When re-engaging, the material is usually not moved at rapid traverse but at the speed intended for the milling cutter with &lt;code&gt;G01&lt;/code&gt;.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Good planning of the milling sequence in the CAM can bring an improvement here. As many sections as possible should be connected and produced in one go without retractions. In CAM, rapid traverse should be used for retraction and re-entry in &amp;lsquo;Z&amp;rsquo;.&lt;/p&gt;&#10;&lt;h3 id="how-do-i-reduce-waiting-times"&gt;How do I reduce waiting times?&lt;/h3&gt;&#10;&lt;p&gt;Cooling lubricant switching processes, spindle speed changes or tool changes are associated with waiting times that can usually be easily reduced:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Allow minimal quantity lubrication to flow during the whole job sequence&lt;/li&gt;&#10;&lt;li&gt;using as few tools as possible at the same speed for a product&lt;/li&gt;&#10;&lt;li&gt;Use stops to simplify machine loading and avoid having to set zero points again and again&lt;/li&gt;&#10;&lt;li&gt;If the machine is operated with completely different parameters for engraving and milling operation, it is advisable to install the control software twice with the appropriate parameter sets in each case. This eliminates the need to always change the configuration.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Continue in &lt;a href="https://blog.schallbert.de/en/cnc-tuning-kinematics/"&gt;&lt;em&gt;Part 2&lt;/em&gt;: Turning up kinematic parameters&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;Step losses occur when the axis of a stepper motor no longer rotates synchronously with the step sequence given by the controller, but instead travels fewer or more steps than dictated. Step loss is almost always caused by inadequate system design (e.g. insufficient or unstable supply voltage, output stages that are too weak or not adapted to the motor) or overloading of the stepper motor. Rarely, however, step losses can be symptoms of defects such as bearing seizure, winding short circuit or blown buffer capacitors.&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;Trochoidal milling is not a bad practice. It has other strengths, but is rarely useful in engraving.&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>Vacuum clamping</title><link>https://blog.schallbert.de/en/vacuum-clamping/</link><pubDate>Tue, 02 Apr 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/vacuum-clamping/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-02-cuttingforces-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Thumbnail of endmill cutting forces"&#10; title="Vacuum clamping" /&gt;&#10;&lt;p&gt;As I mentioned in the &lt;a href="https://blog.schallbert.de/en/makerfaire-ruhr/#delays"&gt;Maker Faire Ruhr 2024 article&lt;/a&gt;, I had major problems clamping my workpieces for the production of spirographs.&lt;/p&gt;&#10;&lt;p&gt;In this article, I would like to go into this in more detail, analyze the causes and finally present a few possible solutions.&lt;/p&gt;&#10;&lt;p&gt;What I can anticipate: All problems can be solved with knowledge of the causes and a little time, planning and material expenditure.&lt;/p&gt;&#10;&lt;h2 id="the-problem"&gt;The problem&lt;/h2&gt;&#10;&lt;p&gt;As usual, I had defined the path planning for the spirographs in such a way that I first had the inner areas (pockets, parts, holes) milled and then the outer areas. Cut-outs and chamfers were to be produced at the very end. I also planned to machine the parts on both sides so that the backs of the gears would also have broken edges and smooth transitions.&lt;/p&gt;&#10;&lt;h3 id="vacuum-loss"&gt;Vacuum loss&lt;/h3&gt;&#10;&lt;p&gt;In my first attempt, I proceeded as I had already successfully done in the &lt;a href="https://blog.schallbert.de/en/projects/spirograph/"&gt;spirograph project&lt;/a&gt; with the material acrylic glass: The back is machined first, but not milled all the way through. After turning the workpiece, only the chamfer cutter should be used to cut out and break the edges at the same time.&lt;/p&gt;&#10;&lt;p&gt;This worked perfectly well for the first clamping.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2024-04-02-spiro-back.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Back machining of the spirographs.&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;With the second one, however, so much negative pressure was lost when the surface was pierced that the material detached from the vacuum table and took on its original, slight curvature. So I had to stop the milling process and think of another strategy to hold it down.&lt;/p&gt;&#10;&lt;p&gt;At this point, I couldn&amp;rsquo;t really explain why so much more pressure was being lost than during production in acrylic glass.&lt;/p&gt;&#10;&lt;h3 id="cling-film"&gt;Cling film&lt;/h3&gt;&#10;&lt;p&gt;I tried to counteract the pressure loss with cling film. This resulted in several consequential problems: If the cutter passes through the film, it is caught by the cutter when milling closed contours and wrapped around it at lightning speed. The cutter no longer cuts, but only rubs against the material, which very quickly leads to heat marks on the wood.&lt;/p&gt;&#10;&lt;p&gt;But even if I attach the cling film in advance: As I cut out parts, the &lt;em&gt;local negative pressure&lt;/em&gt; on the part is already so small when it first enters the groove already present on the underside that it can hardly be held in place.&lt;/p&gt;&#10;&lt;h3 id="twisting-of-the-workpiece"&gt;Twisting of the workpiece&lt;/h3&gt;&#10;&lt;p&gt;For the second attempt, I adapted the strategy: The end mill would mill through the workpiece, so I didn&amp;rsquo;t need a second set-up. I wanted to optionally realize the edge breaking on the back side later via a holder clamped on the table, into which I would be able to insert the parts.&lt;/p&gt;&#10;&lt;p&gt;While cutting out the parts still worked without any problems (again, milling the &amp;ldquo;inner&amp;rdquo; parts first), I ran into major problems when applying the chamfer to the front. The gears began to twist and the template with the ring gear could not be held down at all.&lt;/p&gt;&#10;&lt;h3 id="too-little-time"&gt;Too little time&lt;/h3&gt;&#10;&lt;p&gt;I therefore had to resort to using holders specially made for the workpieces when machining the front side. Their design and manufacture took time that I had not planned for at this point.&lt;/p&gt;&#10;&lt;p&gt;Before the trade fair, I wasn&amp;rsquo;t able to do an in-depth analysis - a quick solution had to be found.&lt;/p&gt;&#10;&lt;h2 id="the-causes"&gt;The causes&lt;/h2&gt;&#10;&lt;p&gt;With a few weeks&amp;rsquo; hindsight, I can now say that several factors led to the problems. A difficult geometry, unfavorable material properties and certain peculiarities of the vacuum table design all came together. But let&amp;rsquo;s get to the bottom of it all here.&lt;/p&gt;&#10;&lt;h3 id="workpiece"&gt;Workpiece&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-02-rawmaterial.jpg" alt="Image: Veneered MDF raw material"&gt;&lt;/figure&gt;&#10;The raw material is medium density fiberboard of &lt;code&gt;10mm&lt;/code&gt; thickness. It is covered on one side with &lt;code&gt;0.75mm&lt;/code&gt; thick olive wood veneer. This material is inherently problematic for processing on a vacuum table:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Due to veneering and storage, the boards are slightly curved&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;, so they want to stand out from my vacuum table.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;MDF is permeable to air, so the vacuum pump has to deal with larger leakage currents than usual (I only get &lt;code&gt;300mBar&lt;/code&gt; when clamping the panel instead of the usual &lt;code&gt;800mBar&lt;/code&gt; without leakage currents). The low material thickness intensifies this effect.&lt;/li&gt;&#10;&lt;li&gt;MDF is not only permeable to air vertically to the panel plane, but also horizontally. This greatly reduces the holding forces in the vicinity of milled grooves, as air can now also penetrate the material laterally, thus additionally increasing the leakage currents.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The video linked here &lt;a href="https://www.youtube.com/watch?v=osJik4XjZTY" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Youtube: Displacement force measurements by &lt;em&gt;Anton CNC&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; clearly shows how different materials behave on a vacuum table with a porous surface. Also highly recommended on this topic is this video &lt;a href="https://www.youtube.com/watch?v=jk7Clrvtlmo" target="_blank" rel="noopener noreferrer" class="external-link"&gt;YouTube: Determining holding force during machining by &lt;em&gt;vakuumtischDE&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; to get an impression of the load the workpiece can be under during the milling process.&lt;/p&gt;&#10;&lt;h3 id="geometry"&gt;Geometry&lt;/h3&gt;&#10;&lt;p&gt;My design for the spirographs is not particularly easy to hold down:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;The template with ring gear has thin walls of only about &lt;code&gt;2cm&lt;/code&gt; thickness. They are thickest on the left and right sides, but have a large recess in the middle through which air can penetrate the material in all directions, thus reducing the holding forces.&lt;/li&gt;&#10;&lt;li&gt;The gear wheel has holes to accommodate pins. The contact pressure is reduced around the holes.&lt;/li&gt;&#10;&lt;li&gt;The teeth of the ring gear and gearwheel are so exposed that they can make virtually no contribution to the holding force due to cross currents of air in the material. At the same time, however, the tooth heads provide a large lever for twisting or shifting the workpiece: an unfavorable pairing.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-02-spirograph-on-vacuum-table.jpg" alt="Image: Spirograph on vacuum table: hole grid overlay"&gt;&lt;/figure&gt;&#10;&lt;p&gt;If I take this as a basis for practice, only the vacuum holes inside the circle in the image above are capable of generating any significant clamping forces. However, these are again close to the center and therefore offer little resistance to twisting of the workpiece. In addition, the gear wheel here has several holes and therefore additional &amp;ldquo;leaks&amp;rdquo;.&lt;/p&gt;&#10;&lt;h3 id="milling-forces"&gt;Milling forces&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-02-cuttingforces-z.jpg" alt="Image: Cutting forces XY"&gt;&lt;/figure&gt;&#10;The forces introduced into the material by the tool (the milling cutter) always act in the XY direction and, in the case of V-cut milling cutters and end mills with spiral/twist, also in the Z direction.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Forces due to feed: The faster the material is moved through, the stronger the forces in the XY direction act on the material in the direction of travel.&lt;/li&gt;&#10;&lt;li&gt;Forces due to cutter rotation (conventional or climb-milling): If an end mill without twist is used, forces are generated orthogonally in the plane of the traverse direction. So if I move in the positive X-direction, the milling cutter will also exert a force in the Y-direction, which is introduced by its own rotation: In conventional milling operation, the cutter wants to avoid the material, so it is deflected towards &lt;code&gt;Y+&lt;/code&gt; (as &lt;a href="https://blog.schallbert.de/en/cnc-router-overload/"&gt;clearly shown in this article&lt;/a&gt;). In the opposite direction, the deflection direction is inverse and the cutter wants to deflect towards &lt;code&gt;Y-&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Forces due to cutter geometry: If the cutter has a spiral or is angled in its geometry, additional forces act in the Z direction. With Vcut and downcut cutters, the material is pressed onto the machine bed, whereas with upcut spiral cutters, the chip is ejected as desired. However, the workpiece also experiences a force that can lift it off the machine bed in extreme cases.&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-02-cuttingforces-xy.jpg" alt="Image: Cutting forces Z"&gt;&lt;/figure&gt;&lt;/li&gt;&#10;&lt;li&gt;If milling is carried out in the full groove (i.e. the milling cutter is engaged over its entire diameter instead of moving along the edge of the workpiece), the forces transverse to the milling direction increase even more: In the image, it moves in the opposite direction at the upper edge of the groove, which pushes the milling cutter &amp;ldquo;away from the material&amp;rdquo;, i.e. downwards in the image. At the same time, it moves in synchronization at the lower edge of the groove, which pulls it &amp;ldquo;into the material&amp;rdquo; - also downwards.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="vacuum-clamping-system"&gt;Vacuum clamping system&lt;/h3&gt;&#10;&lt;p&gt;As described in the articles &lt;a href="https://blog.schallbert.de/en/cnc-vacuum-pumps/"&gt;about vacuum pumps&lt;/a&gt; and &lt;a href="https://blog.schallbert.de/en/why-vacuum-table/"&gt;vacuum tables&lt;/a&gt;, I have a dry-running rotary vane pump connected to a perforated grid vacuum table. When processing porous materials, this combination is just as problematic as grid vacuum tables with small-volume pumps:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;My hole-grid vacuum table has blind holes with a vacuum channel of &lt;code&gt;0.5mm&lt;/code&gt; diameter in their center. This constriction relieves the pump when holes are uncovered, as it does not have to provide the full air flow that the &lt;code&gt;5mm&lt;/code&gt; bore allows. However, the hole can build up less negative pressure due to its limited air flow in combination with MDF.&lt;/li&gt;&#10;&lt;li&gt;Due to the high leakage currents, my vacuum pump is at its limit with its volume flow. Other pump types are much better suited here (see &lt;a href="https://blog.schallbert.de/en/cnc-vacuum-pumps/#radial-blower"&gt;radial blower&lt;/a&gt;, &lt;a href="https://blog.schallbert.de/en/cnc-vacuum-pumps/#sidechannel-compressor"&gt;side channel compressor&lt;/a&gt;).&lt;/li&gt;&#10;&lt;li&gt;A vacuum table generally only clamps downwards (in the Z direction). However, forces in the XY direction must also be absorbed to prevent the workpiece from shifting or rotating. These are coupled to the vacuum on the workpiece via the &lt;a href="https://blog.schallbert.de/en/cnc-vacuum-pumps/#lateral-force"&gt;coefficient of static friction µ&lt;/a&gt; (and can be influenced by material pairing and surface properties), but if the latter is too low, the workpiece is not held reliably.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-02-vacuumtable-channels-porousmaterials.jpg" alt="Image: Air flow system of vacuum table and porous material"&gt;&lt;/figure&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Although the vacuum channels are located in a &lt;code&gt;10mm&lt;/code&gt; grid, the distance relevant for the leakage flow is only &lt;code&gt;5mm&lt;/code&gt;. This is because the edges of the blind holes (each &lt;code&gt;5mm&lt;/code&gt; in diameter) are at this distance from each other, so that the air flow has to travel a shorter distance across the material than through it.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h4 id="comparison-with-the-resistance-network"&gt;Comparison with the resistance network&lt;/h4&gt;&#10;&lt;p&gt;The actual clamping force can be easily estimated by imagining the pump, vacuum table and workpiece as an electrical network. I have prepared a model for this and present it below.&lt;/p&gt;&#10;&lt;h4 id="resistors-vacuum-table-workpiece"&gt;Resistors: Vacuum table, workpiece&lt;/h4&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-02-tableworkpiece.jpg" alt="Image: Vacuum table and workpiece"&gt;&lt;/figure&gt;&#10;The suction holes of the vacuum table represent resistors for the air flow, which are connected in parallel. The workpiece is again connected in series as a resistor, which is connected with its other pole to &amp;ldquo;ground&amp;rdquo;, i.e. the ambient air pressure.&lt;/p&gt;&#10;&lt;h4 id="vacuum-manifold--switch-pressure-gauge--voltmeter"&gt;Vacuum manifold = switch, pressure gauge = voltmeter&lt;/h4&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-02-manoswitch.jpg" alt="Image: Vacuum manifold and manometer"&gt;&lt;/figure&gt;&#10;The pressure gauge in the suction channel can be seen as a voltage meter. I take the vacuum distributor with its valves for switching the vacuum sections as an electrical switching device. If the switch is closed, it establishes the connection between pump and vacuum table. If it is open, it blocks the air flow.&lt;/p&gt;&#10;&lt;h4 id="vacuum-pump--voltage-source"&gt;Vacuum pump = voltage source&lt;/h4&gt;&#10;&lt;p&gt;In this picture, the pump represents a voltage source, which in my case can provide a maximum of &lt;code&gt;U = 8V&lt;/code&gt; corresponding to &lt;code&gt;-800mBar&lt;/code&gt;. Due to the internal resistance of &lt;code&gt;0.2Ohm&lt;/code&gt; present in real voltage sources, it can generate a short-circuit current of &lt;code&gt;I = 40A&lt;/code&gt;, analogous to the &lt;code&gt;40m³/h&lt;/code&gt; flow rate. This comparison is of course not physically correct, because I would have to work with mass flows instead of volume flows. In addition, the flow rate is not linearly related to the pressure everywhere in the characteristic diagram and the resistance values I have assumed are only reference values. In my opinion, it is nevertheless suitable for modeling the situation under steady-state conditions.&lt;/p&gt;&#10;&lt;p&gt;With workpiece materials such as acrylic glass or metal, the air flow is close to zero, so their &amp;ldquo;resistance&amp;rdquo; is extremely high. This causes most of the tension (negative pressure) to drop here, so that the workpiece is firmly clamped.&lt;/p&gt;&#10;&lt;p&gt;When using airtight materials, all is well with the world: the pressure gauge correctly indicates the pressure on the material when the workpiece is still unprocessed. Even in the case of milled holes, only the resistance of the material &amp;ldquo;in series&amp;rdquo; to the affected suction holes is set to zero. This means that the negative pressure and therefore the holding force on the part itself is hardly reduced.&lt;/p&gt;&#10;&lt;p&gt;If you look closely, you can see that the pressure gauge measures the pressure difference upstream of the vacuum manifold. Its value is therefore only meaningful for the clamping force if the workpiece has a very high flow resistance.&lt;/p&gt;&#10;&lt;h4 id="special-case-of-porous-materials"&gt;Special case of porous materials&lt;/h4&gt;&#10;&lt;p&gt;The situation is completely different with porous materials such as MDF:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-02-porousmaterial-circuitdiagram.jpg" alt="Image: equivalent circuit diagram for porous material on a vacuum table"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Here, the contact resistance of the material is only just an order of magnitude higher than that of the vacuum holes. I determined the values through tests on the vacuum table. They are based on pressure measurements, calculations of the workpiece surface, and the number of active suction holes covered by the material. Two effects should be particularly emphasized here, which made holding down even more difficult for me:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Pay attention to the resistances in the material in parallel to the contact resistance in the &lt;code&gt;Z direction&lt;/code&gt;. They are caused by the fact that the air can now also move through the material &amp;ldquo;between the holes&amp;rdquo;.&lt;/li&gt;&#10;&lt;li&gt;On the right-hand side of the picture you can see what happens when the workpiece protrudes into an inactive vacuum area: The suction holes are connected to each other with virtually no resistance by means of vacuum channels in the table. As the workpiece allows &amp;ldquo;transverse air flow&amp;rdquo; in the &lt;code&gt;XY&lt;/code&gt; direction, significantly less resistance has to be overcome by connecting the suction holes than for penetration in the &lt;code&gt;Z&lt;/code&gt; direction. This means that only &lt;code&gt;R29&lt;/code&gt; and &lt;code&gt;R22||R21&lt;/code&gt; are still in series. If the workpiece covers several inactive hole fields, &lt;code&gt;R22&lt;/code&gt; approaches 0.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;As a result, the resistances upstream of &lt;code&gt;R19&lt;/code&gt;, namely &lt;code&gt;R18||(R29+(R21||R22))&lt;/code&gt;, become so small that the pump practically only draws ambient air here and can hardly apply any clamping force at this point. The cross air flow also affects the pressure drop of &lt;code&gt;R17, R15&lt;/code&gt; via &lt;code&gt;R25&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Now the small suction holes act as voltage dividers, so that a not inconsiderable part of the negative pressure &amp;ldquo;sticks&amp;rdquo; to them.&lt;/p&gt;&#10;&lt;p&gt;This means that the pressure displayed on the pressure gauge no longer corresponds to the conditions on the workpiece: Although I still get &lt;code&gt;-200mBar&lt;/code&gt; displayed here, the clamping force is reduced precisely at the most sensitive points for holding down&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-02-resnetwork.jpg" alt="Image: Simplified equivalent circuit diagram for a vacuum clamping system"&gt;&lt;/figure&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;at the edge of the workpiece&lt;/li&gt;&#10;&lt;li&gt;in areas adjacent to those where the workpiece is not actively sucked in&lt;/li&gt;&#10;&lt;li&gt;as well as at every through-milling and a certain radius&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;to almost zero.&lt;/p&gt;&#10;&lt;p&gt;For all other areas, &lt;a href="https://en.wikipedia.org/wiki/Kirchhoff%27s_circuit_laws" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Kirchhoff&amp;rsquo;s laws&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; apply: A voltage divider is formed consisting of the pump&amp;rsquo;s internal resistance, the vacuum system and the penetration resistance of the workpiece. Only the voltage still on the workpiece determines the hold-down force. It is - I repeat myself now - possibly &lt;em&gt;much lower&lt;/em&gt; than a glance at the pressure gauge would suggest.&lt;/p&gt;&#10;&lt;h2 id="solutions"&gt;Solutions&lt;/h2&gt;&#10;&lt;p&gt;Quite a lot of theory for the banal realization that the workpiece is twisted on the machine bed, isn&amp;rsquo;t it? I have the following solutions in my quiver for such a problem:&lt;/p&gt;&#10;&lt;h3 id="pump--vacuum-table"&gt;Pump / vacuum table&lt;/h3&gt;&#10;&lt;p&gt;When working with porous materials, it is important that the table itself has a low flow resistance. This is the only way to ensure that the majority of the negative pressure falls on the workpiece, resulting in a higher clamping force. According to the motto &amp;ldquo;a lot helps a lot&amp;rdquo;, a pump with a higher flow rate, a table with larger suction openings and a vacuum distributor with larger hose diameters would be appropriate here. However, this is out of question for me.&lt;/p&gt;&#10;&lt;h3 id="adhesion-agent"&gt;Adhesion agent&lt;/h3&gt;&#10;&lt;p&gt;I could try using a coarser-meshed rubber mat. With a &lt;code&gt;20mm&lt;/code&gt; hole pattern, the resistance for the cross air flow is tripled, so that the outer areas are no longer affected by the pressure loss to such an extent. Such a mat also has more rubber surface area, which increases static friction.&lt;/p&gt;&#10;&lt;p&gt;Adhesive tape instead of cling film on the material surface solves the problem of catching in the cutter and closes the pores of the material locally, which can improve the holding force in critical areas.&lt;/p&gt;&#10;&lt;p&gt;Of course, conventional methods such as applying double-sided adhesive tape or screwing down the workpiece can also be used on a vacuum table. But I don&amp;rsquo;t do that - because then I would have to admit to myself that my vacuum table is not suitable for all cases.&lt;/p&gt;&#10;&lt;h3 id="fixture-and-taps"&gt;Fixture and taps&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-04-02-spirofixtures.jpg" alt="Image: Fixture for holding my gears in place"&gt;&lt;/figure&gt;&#10;This solution aims to prevent the workpiece from twisting and shifting. The vacuum table is only used here to hold down the workpiece, while a suitably manufactured holder absorbs forces in the &amp;lsquo;XY direction&amp;rsquo;. The picture shows holders for the template and the gear wheel, which fulfill this task. They are attached to the machine bed and require a precise zero point in order to be able to break the workpieces edges without misalignment.&lt;/p&gt;&#10;&lt;p&gt;I use a clearance fit with &lt;code&gt;0.1mm&lt;/code&gt; between the components. If the clearance is smaller, I need too much force to insert. If I use larger values, the workpieces can be twisted too much.&lt;/p&gt;&#10;&lt;p&gt;With this variant, I can straighten warped workpieces, continue to produce very fine engravings and still make sure that nothing slips.&lt;/p&gt;&#10;&lt;p&gt;The disadvantages are time and material required for planning and producing individual fixtures or templates.&lt;/p&gt;&#10;&lt;h3 id="modification-of-the-workpiece"&gt;Modification of the workpiece&lt;/h3&gt;&#10;&lt;p&gt;This is obvious: I can change the properties of the workpiece so that it becomes impermeable to air. For example, I can cover the underside with a film to improve the static friction.&lt;/p&gt;&#10;&lt;p&gt;If, like me, you don&amp;rsquo;t like using plastic: I have found that baking paper / cling film made from unbleached cellulose hardly lets any air through. I&amp;rsquo;ll soon be trying out covering materials or covering areas affected by milling with it instead of plastic film.&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;If such a problem occurs regularly, grid vacuum tables with a sealing cord are an excellent solution: Here, a linear seal is used to seal the outer edges of the workpiece against the ambient air pressure so that the edges of the workpiece have no chance of curving upwards. With my perforated grid table, however, the entire surface of the rubber mat acts as a seal, so that the curvature is in the form of a contact line on the mat. However, lines do not have a surface area, and my table can only hold down through this. Incidentally, this is also the reason why flexible materials such as films can be pulled off the vacuum table so easily.&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>Maker Faire Ruhr 2024</title><link>https://blog.schallbert.de/en/makerfaire-ruhr/</link><pubDate>Mon, 25 Mar 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/makerfaire-ruhr/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-02-23-maker-faire-logo-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Maker Faire Ruhr Logo / Banner"&#10; title="Maker Faire Ruhr 2024" /&gt;&#10;&lt;h2 id="preparation"&gt;Preparation&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-02-23-maker-faire-logo.jpg" alt="Image: Maker Faire Logo"&gt;&lt;/figure&gt;&#10;Since the beginning of January, two of my friends and I have been preparing for the &lt;a href="https://www.makerfaire-ruhr.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Maker Faire Ruhr&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; at the &lt;a href="https://www.dasa-dortmund.de/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;DASA Dortmund&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. In the course of this, I veneered fiberboard, cut out blanks for engravings, procured tablecloths, machined PC housings, milled gear wheels, programmed LED strips, designed a stand sign, set up a CNC machine, optimized it for engraving and so on&amp;hellip;&lt;/p&gt;&#10;&lt;p&gt;As the deadline approached, things got tight. A lot of time and coordination goes into preparing for a trade faire like this.&lt;/p&gt;&#10;&lt;h3 id="projects"&gt;Projects&lt;/h3&gt;&#10;&lt;p&gt;At the faire I wanted to present some of my most beautiful projects such as &lt;a href="https://blog.schallbert.de/en/projects/seifenbutler/"&gt;Seifenbutler&lt;/a&gt;, my oversized &lt;a href="https://blog.schallbert.de/en/projects/spirograph/"&gt;spirographs&lt;/a&gt; and allow visitors to have their own signs &lt;a href="https://blog.schallbert.de/en/negative-carving-with-estlcam/"&gt;engraved&lt;/a&gt;, even with &lt;a href="https://blog.schallbert.de/en/projects/qr-codengrave/"&gt;QR code&lt;/a&gt; if required.&lt;/p&gt;&#10;&lt;p&gt;To make the milling work quicker, I made blanks that only had to be engraved later. I also had to make the spirographs in advance because of the double-sided machining required.&lt;/p&gt;&#10;&lt;h3 id="machines"&gt;Machines&lt;/h3&gt;&#10;&lt;p&gt;I was able to convince a few companies to lend me machines and parts for the faire:&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--left"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/sorotec.jpg" alt="Image: Sorotec logo"&gt;&lt;/figure&gt;&#10;From &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; I borrowed a &amp;ldquo;Hobbyline4530&amp;rdquo;, their smallest and lightest machine.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--left"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/stritzelberger.jpg" alt="Bild: Stritzelberger Logo"&gt;&lt;/figure&gt;&#10;The company &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; provided me with a vacuum table that matches the machine, including a suitable pump.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--left"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/eding.jpg" alt="Image: EdingCNC logo"&gt;&lt;/figure&gt;&#10;&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; supported me in setting up their software, lent me exhibitors for the latest control hardware and finally even came by my stand live for an exchange.&lt;/p&gt;&#10;&lt;h3 id="the-stand-sign"&gt;The stand sign&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-02-23-schallbert-standschild.jpg" alt="Image: My plan for a great booth sign"&gt;&lt;/figure&gt;&#10;I came up with something very special for the stand sign: a planetary gear! Three moving gears revolve around a fixed sun wheel. In order to be able to suspend the stand shield and keep the gear without edges, a centered axle for the planetary carriers was out of the question.&lt;/p&gt;&#10;&lt;p&gt;This task is performed by a ring gear concealed by the sun gear and held by three ball bearings (image: arranged at a 120° angle, mounted on the teeth of the sun gear). It is driven by a stepper motor with a reduction ratio of 1:5. The ring gear has mounts for the planet carriers. This frees up the middle section of the sun wheel, where I can fit the drive motor, lighting and shield mounts.&lt;/p&gt;&#10;&lt;p&gt;A video on my PeerTube channel shows how I built the stand sign using my CNC.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;iframe&#10; src="https://makertube.net/videos/embed/xsLhWWygyJbtJGwUGrKvVE"&#10; title="Making of Standschild (Maker Faire Ruhr)"&#10; loading="lazy"&#10; allow="autoplay; fullscreen; picture-in-picture"&#10; allowfullscreen&#10; style="width: 100%; height: auto; min-height: 315px; aspect-ratio: 16/9;"&#10; &gt;&lt;/iframe&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Making of Standschild (Maker Faire Ruhr)&lt;/span&gt;&lt;a href="https://makertube.net/videos/embed/xsLhWWygyJbtJGwUGrKvVE" class="attr-link" target="_blank" rel="noopener noreferrer"&gt;↗&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h3 id="delays"&gt;Delays&lt;/h3&gt;&#10;&lt;p&gt;As always with such major undertakings, a few things went wrong during the preparations:&lt;/p&gt;&#10;&lt;h4 id="detachment-of-the-vacuum-during-the-production-of-the-spirographs"&gt;Detachment of the vacuum during the production of the spirographs&lt;/h4&gt;&#10;&lt;p&gt;I wanted to make the spirographs from veneered MDF, as the material is very attractive to look at and at the same time stable enough not to be damaged by rough handling. So I covered &lt;code&gt;10mm&lt;/code&gt; thick medium density fiberboard with olive wood veneer (time consuming!) and then routed it on my portal router.&lt;/p&gt;&#10;&lt;p&gt;As I use a &lt;a href="https://blog.schallbert.de/en/why-vacuum-table/"&gt;vacuum clamping system&lt;/a&gt;, it is key to achieve a certain vacuum level. Otherwise the workpieces won&amp;rsquo;t stay in place. MDF is permeable to air, so the pump has a lot to do even when the panel has not yet been processed. Unfortunately, the geometry of my design for the spirographs is such that the surface remaining for clamping continues to decrease considerably during machining.&lt;/p&gt;&#10;&lt;p&gt;In the end, I had the problem that the gears wanted to turn at the latest when machining the edges of the back. Even my desperate attempts to maintain sufficient negative pressure by applying large amounts of cling film to the workpiece failed.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&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;p&gt;So instead of the planned ten spirographs, I only received four - the rest were rejects. &lt;em&gt;Note&lt;/em&gt;: MDF can only be used on my vacuum clamping system for large workpieces.&lt;/p&gt;&#10;&lt;h4 id="problematic-gear-pairing-on-the-spirograph"&gt;Problematic gear pairing on the spirograph&lt;/h4&gt;&#10;&lt;p&gt;Unfortunately, the first spirograph made of MDF was not really usable: one tooth was constantly jammed on the flank of the ring gear and refused to mesh. The reason for this was that I had not provided any backlash and had manufactured the tooth heads without a radius.&lt;/p&gt;&#10;&lt;p&gt;As a result, the gear wheel must always be pressed firmly against the ring gear to enable the next tooth engagement. If the gear wheel slips out just a few tenths of a millimeter, it locks. &lt;em&gt;Remedy&lt;/em&gt;: Provide both gear and ring gear tooth heads with &lt;code&gt;r=3.0mm&lt;/code&gt; and design the gear pairing with backlash of &lt;code&gt;a=0.25mm&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h4 id="incorrectly-engraved-lettering-on-the-large-stand-plate"&gt;Incorrectly engraved lettering on the large stand plate&lt;/h4&gt;&#10;&lt;p&gt;I made two mistakes here and unfortunately produced a lot of rejects. On the first attempt, I had overlooked to mirror the gear wheel for the rear machining. I thought this wasn&amp;rsquo;t necessary because the gear itself is mirror-symmetrical. Unfortunately, however, the retaining pins were not embedded with mirror symmetry, so that the back was not aligned with the front.&lt;/p&gt;&#10;&lt;p&gt;On the second attempt - see video above - I had fixed this, but forgot to adjust the zero point according to the new centering pin position on the back. Fortunately, I noticed this during processing and was able to correct it.&lt;/p&gt;&#10;&lt;h4 id="retro-pc-window-production"&gt;Retro-PC window production&lt;/h4&gt;&#10;&lt;p&gt;On one of the three retro PCs, I placed the back of the housing on the machine bed rotated by 180°. The window was therefore milled &amp;ldquo;upside down&amp;rdquo;, which I only noticed after finishing the milling job. Fortunately, my friend was able to file and bend the latches so that the back of the housing could also be attached to the PC &amp;ldquo;upside down&amp;rdquo;.&lt;/p&gt;&#10;&lt;p&gt;During the production of the last window, the chip guard got caught in the workpiece&amp;rsquo;s splay and broke because I couldn&amp;rsquo;t get the machine to stop in time. This could only happen because the chip guard became loose due to the vibrations during machining. &lt;em&gt;Note&lt;/em&gt;: The milling machine is not a toy. So don&amp;rsquo;t make any compromises or use half-baked makeshift solutions!&lt;/p&gt;&#10;&lt;h2 id="the-maker-faire-ruhr-2024"&gt;The Maker Faire Ruhr 2024&lt;/h2&gt;&#10;&lt;p&gt;The fair itself was great. I received a very friendly welcome and DASA gave me excellent support with unloading and all other logistics. The stand assigned to me was also great: very large, well located, with plenty of power sockets - what more could you want?&lt;/p&gt;&#10;&lt;p&gt;I had a company visit my stand, family and friends came and even some people from the CNC forums and a few colleagues from work came to see me. I couldn&amp;rsquo;t have wished for more.&lt;/p&gt;&#10;&lt;h3 id="set-up"&gt;Set-up&lt;/h3&gt;&#10;&lt;p&gt;Fortunately, we had a forklift to help us unload.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2024-03-25-unloading.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Unloading the CNC&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;Everything else was manual work and not much different from setting up for a music concert: Moving tables, laying power cables, installing the lighting system, setting up exhibits.&lt;/p&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&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-0"&#10; aria-label="Image: Vacuumpump and -table"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2024-02-23-vacuumpump.jpg"&#10; alt="Image: Vacuumpump and -table"&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-1"&#10; aria-label="Image: CNC machine, ready to run."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2024-02-23-hobbyline_readytorun.jpg"&#10; alt="Image: CNC machine, ready to run."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-2"&#10; aria-label="Image: Signs for my projects that I&amp;#39;ll showcase."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2024-02-23-standschilder.jpg"&#10; alt="Image: Signs for my projects that I&amp;#39;ll showcase."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-3"&#10; aria-label="Image: Retrogaming sign for another booth"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2024-02-23-retrogaming_sign.jpg"&#10; alt="Image: Retrogaming sign for another booth"&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-4"&#10; aria-label="Image: Beautiful scrap - residue piece of PMMA in form of a wave"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2024-02-23-pmma_wave.jpg"&#10; alt="Image: Beautiful scrap - residue piece of PMMA in form of a wave"&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-5"&#10; aria-label="Image: The side panel of a midi-tower PC: now with a window"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2024-02-23-pcwindow_sea.jpg"&#10; alt="Image: The side panel of a midi-tower PC: now with a window"&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-6"&#10; aria-label="Image: Schallbert&amp;#39;s booth on Maker Faire Ruhr 2024"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2024-03-25-DerStand.jpg"&#10; alt="Image: Schallbert&amp;#39;s booth on Maker Faire Ruhr 2024"&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-7"&#10; aria-label="Image: 2000s-retro-PCs set up and ready to play."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2024-03-25-Gaming.jpg"&#10; alt="Image: 2000s-retro-PCs set up and ready to play."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-8"&#10; aria-label="Image: CNC machine detail view"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2024-03-25-StandDetail.jpg"&#10; alt="Image: CNC machine detail view"&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-9"&#10; aria-label="Image: Visitors on Saturday"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2024-03-25-Besucher.jpg"&#10; alt="Image: Visitors on Saturday"&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-10"&#10; aria-label="Image: Schallbert&amp;#39;s booth, rear view"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2024-03-25-Backstage.jpg"&#10; alt="Image: Schallbert&amp;#39;s booth, rear view"&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-11"&#10; aria-label="Image: Many more visitors on Sunday."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2024-03-25-Menschenmenge.jpg"&#10; alt="Image: Many more visitors on Sunday."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;/div&gt;&lt;div class="hugo-gallery__caption"&gt;&#10; Impressionen von der Maker Faire Ruhr 2024.&#10; &lt;/div&gt;&lt;/div&gt;&#10;&#10; &lt;div class="hugo-gallery__full" aria-live="polite"&gt;&lt;figure&#10; id="gallery-full-0"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2024-02-23-vacuumpump.jpg"&#10; alt="Image: Vacuumpump and -table"&gt;&lt;figcaption&gt;Hier wird der Vakuumtisch in Betrieb genommen und getestet.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-1"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2024-02-23-hobbyline_readytorun.jpg"&#10; alt="Image: CNC machine, ready to run."&gt;&lt;figcaption&gt;Die CNC-Maschine für die Messe. Fertig eingerichtet und betriebsbereit.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-2"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2024-02-23-standschilder.jpg"&#10; alt="Image: Signs for my projects that I&amp;#39;ll showcase."&gt;&lt;figcaption&gt;Selbstgefräster Schilder für die dargestellten Projekte.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-3"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2024-02-23-retrogaming_sign.jpg"&#10; alt="Image: Retrogaming sign for another booth"&gt;&lt;figcaption&gt;Das Standschild für einen guten Freund, der Retro-PCs und -Spiele ausstellt.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-4"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2024-02-23-pmma_wave.jpg"&#10; alt="Image: Beautiful scrap - residue piece of PMMA in form of a wave"&gt;&lt;figcaption&gt;Schöner Abfall - ein Reststück Acrylglas in Form einer Welle&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-5"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2024-02-23-pcwindow_sea.jpg"&#10; alt="Image: The side panel of a midi-tower PC: now with a window"&gt;&lt;figcaption&gt;Das Gegenstück zur Welle: Ein PC mit einzigartigem Fenster, selbstgefräst.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-6"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2024-03-25-DerStand.jpg"&#10; alt="Image: Schallbert&amp;#39;s booth on Maker Faire Ruhr 2024"&gt;&lt;figcaption&gt;Die LED-Streifen sind fertig programmiert und alles steht an seinem Platz.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-7"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2024-03-25-Gaming.jpg"&#10; alt="Image: 2000s-retro-PCs set up and ready to play."&gt;&lt;figcaption&gt;Retro-2000er-PCs sind aufgebaut und vernetzt. Bereit zum Spielen.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-8"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2024-03-25-StandDetail.jpg"&#10; alt="Image: CNC machine detail view"&gt;&lt;figcaption&gt;Die betriebsbereite CNC-Maschine auf der Messe.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-9"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2024-03-25-Besucher.jpg"&#10; alt="Image: Visitors on Saturday"&gt;&lt;figcaption&gt;Bereits einige Besucher, Maker und Interessierte am Samstag.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-10"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2024-03-25-Backstage.jpg"&#10; alt="Image: Schallbert&amp;#39;s booth, rear view"&gt;&lt;figcaption&gt;Backstage des betriebsbereiten Standes.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-11"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2024-03-25-Menschenmenge.jpg"&#10; alt="Image: Many more visitors on Sunday."&gt;&lt;figcaption&gt;Doch am Sonntag war noch viel mehr los.&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&#10;&lt;/section&gt;&#10;&#10;&lt;h3 id="visitors"&gt;Visitors&lt;/h3&gt;&#10;&lt;p&gt;I can draw a positive balance for my first Maker Faire: There were really a lot of people at our stand and I hardly had a few minutes to rest - there was always something to talk about, to present my projects or to talk about the machine on display. But that was exactly what attracted us to this format. What&amp;rsquo;s more, the technology worked, the machine ran smoothly and all the things I had prepared so long in advance (such as the stand sign) could be exhibited or shown in operation.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2024-03-25-short_engrave.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;QR-Codengrave Demonstration&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h3 id="what-i-would-still-do-differently-next-time"&gt;What I would still do differently next time&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;The Maker Faire is not a sales fair in terms of setup, but a huge show &amp;amp; tell. That&amp;rsquo;s why I wouldn&amp;rsquo;t book a paid sales stand again, because in my case it wasn&amp;rsquo;t worth it.&lt;/li&gt;&#10;&lt;li&gt;One or two more people to look after the stand would be great. Then we could take turns.&lt;/li&gt;&#10;&lt;li&gt;More material for the spirographs! All of the pens we brought were broken by the end of the event and had run out of paper by the middle of the second day.&lt;/li&gt;&#10;&lt;li&gt;More presentations / demos. The milling machine was often running, but there was a lack of templates and requests from the visitors. Next time I will take more of my own designs with me and produce them for demo purposes even without a request.&lt;/li&gt;&#10;&lt;li&gt;Prepare better links to the website, because a QR code alone doesn&amp;rsquo;t help everyone.&lt;/li&gt;&#10;&lt;li&gt;More coffee: The supply of hot drinks (and now that I write it, food too) was undersized and unfortunately not as tasty as I would have liked. So it&amp;rsquo;s better to make own sandwiches and bring own thermos flasks.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item><item><title>Seifenbutler - Pilot run!</title><link>https://blog.schallbert.de/en/seifenbutler-pilot-run/</link><pubDate>Thu, 08 Feb 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/seifenbutler-pilot-run/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-02-08-seifenbutler-white.jpg"&#10; class="post-cover"&#10; alt="Image: Seifenbutler ready for sale"&#10; title="Seifenbutler - Pilot run!" /&gt;&#10;&lt;p&gt;My Hardware project &lt;a href="https://blog.schallbert.de/en/projects/move-blog-to-own-server/"&gt;Seifenbutler&lt;/a&gt; is complete. After more than a year of development, I have manufactured some in white, black, transparent, and white translucent. They even made it to a first &lt;a href="https://mopemaster.com/produkt/seifenbutler-seifenhalter-dusche/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;online shop&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-02-08-seifenbutler-collection.jpg" alt="Image: Seifenbutler in three different colours"&gt;&lt;/figure&gt;&#10;&lt;h2 id="how-do-i-determine-the-sales-price"&gt;How do I determine the sales price?&lt;/h2&gt;&#10;&lt;p&gt;As I use high-quality materials and the production and assembly time is long, I don&amp;rsquo;t have much room for maneuver when it comes to pricing. On the other hand, I can&amp;rsquo;t ask moon prices for a commodity like Seifenbutler - unless I use precious woods or other unusual or complex materials. There is a lot of material online for such calculations. I used a &lt;a href="https://www.youtube.com/watch?v=mxwggzhe_hs" target="_blank" rel="noopener noreferrer" class="external-link"&gt;YouTube video from the &amp;ldquo;Cutting It Close&amp;rdquo; channel&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; as a guide.&lt;/p&gt;&#10;&lt;p&gt;My price calculation is made up of the following items:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;Final price = material price&#10; + machine time (investment, wear, tools, consumables)&#10; + production and assembly time &#10; + packaging costs &#10; + fees (postage, payment service providers, stores)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;As you can see, I have not included my own time for product development in the calculation - if I had to do this, it would easily add the equivalent of &lt;code&gt;250h&lt;/code&gt; of working time.&lt;/p&gt;&#10;&lt;h3 id="material-prices"&gt;Material prices&lt;/h3&gt;&#10;&lt;p&gt;&lt;strong&gt;As of the end of 2023&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;Ok, let&amp;rsquo;s quickly list the expenses per part:&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;quantity&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;item&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;price/unit&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;unit used&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;price&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;2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;PMMA 5mm XT&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;90€/m²&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0,03&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2,70€&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;Knurl M4 A2 22mm&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;7,54€/25pcs&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0,04&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0,30€&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;Washer M4 A2 12mm&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0,94€/100pcs&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0,01&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0,01€&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;pin M4 A2 12mm&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;6,70€/50pcs&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0,02&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0,13€&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;5&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;screw M4 A2 8mm&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;12,40€/200pcs&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0,025&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0,31€&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;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Material cost/part&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;3,45€&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="machine-time"&gt;Machine time&lt;/h3&gt;&#10;&lt;p&gt;I estimate the machine time for this project at &lt;code&gt;100€/h&lt;/code&gt;. The lion&amp;rsquo;s share of this is the investment, maintenance and wear costs of the machine at &lt;code&gt;€50/h&lt;/code&gt; plus my own working time, which I simply estimate at &lt;code&gt;€30/h&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;The tool costs are quite high at around &lt;code&gt;€15/h&lt;/code&gt;, as I have to use four different tools, all of which are small in diameter and therefore subject to high wear.&lt;/p&gt;&#10;&lt;p&gt;Consumables are vacuum fleeces, positioning pins, cut protection gloves and adhesive tape. I need the latter to fix the small milled parts and the spiral. Their share of the machine time costs is around &lt;code&gt;5€/h&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;The machine time is currently &lt;code&gt;8min/part&lt;/code&gt;. This means that the item &lt;code&gt;machine time&lt;/code&gt; amounts to &lt;code&gt;13.30€/piece&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="assembly-time"&gt;Assembly time&lt;/h3&gt;&#10;&lt;p&gt;It takes me about 5 minutes to assemble a soap holder. If I apply an hourly rate of &lt;code&gt;30€&lt;/code&gt;, I arrive at &lt;code&gt;2.50€&lt;/code&gt; per item.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-02-08-seifenbutler-assembly.jpg" alt="Image: Seifenbutler ready to assemble"&gt;&lt;/figure&gt;&#10;&lt;h3 id="packaging-costs-and-purchase-of-materials"&gt;Packaging costs and purchase of materials&lt;/h3&gt;&#10;&lt;p&gt;I pack the soap holders in simple DIN A6 envelopes after wrapping them carefully in wrapping paper. The costs here amount to just &lt;code&gt;0.20€&lt;/code&gt; per item. However, if I now include my working time for packaging and shipping labels, we are already at &lt;code&gt;0.90€&lt;/code&gt; per item. Added to this are the costs for postage when purchasing materials and the costs for paper and printing the operating instructions, which I calculate at a further &lt;code&gt;0.50€/unit&lt;/code&gt; - the small quantities produced are weighing heavy on this.&lt;/p&gt;&#10;&lt;h3 id="fees"&gt;Fees&lt;/h3&gt;&#10;&lt;p&gt;Fees are incurred for shipping and payment service providers, for example. Here the prices vary depending on the provider, so I plan a shipping surcharge of &lt;code&gt;3.50€&lt;/code&gt; (shipping by large letter) - this does not apply to sales &amp;ldquo;at the counter&amp;rdquo;.&lt;/p&gt;&#10;&lt;h2 id="the-total-price"&gt;The total price&lt;/h2&gt;&#10;&lt;p&gt;If I add up all the items, I would have to call up a store price of &lt;code&gt;20.15€&lt;/code&gt; (cash) for the unpacked goods and &lt;code&gt;24.75€&lt;/code&gt; in the case of shipping with cashless payment. And then the company would still not have made a cent of profit.&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;m not sure whether the soap butler can be sold for such prices. But if you&amp;rsquo;re honest and don&amp;rsquo;t want to end up paying more, such calculations are unavoidable, at least roughly.&lt;/p&gt;&#10;&lt;h2 id="the-assumed-merchant-with-a-store"&gt;The assumed merchant with a store&lt;/h2&gt;&#10;&lt;p&gt;If I were a merchant&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;, I would still have to add VAT and would also want to see the development costs amortized somehow. I&amp;rsquo;m also not sure whether I would get through the door &amp;ldquo;as a merchant&amp;rdquo; with an hourly rate of &lt;code&gt;30€&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Phew. If you now include things like store rent, costs for storage space, workshop rent, electricity, waste disposal, membership of the &amp;ldquo;Green Dot&amp;rdquo; in accordance with the Packaging Act, contributions to the Chamber of Industry and Commerce, the employers&amp;rsquo; liability insurance association as well as taxes and duties, then even with a further 20% mark-up, a whole heap of soap holders would have to be sold in order not to end up poorer than before!&lt;/p&gt;&#10;&lt;p&gt;Let&amp;rsquo;s assume I can rent a workshop and open a tiny store.&lt;/p&gt;&#10;&lt;h3 id="a-fictitious-store"&gt;A fictitious store&lt;/h3&gt;&#10;&lt;p&gt;Let&amp;rsquo;s do a quick calculation with a few assumed values.&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;Price/Unit&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Used&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Final price&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;Shop&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;16€/m² per month&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;40m²&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;640€&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;Storage space&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;4€/m² per month&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;8m²&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;32€&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;workshop&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;8€/m² per month&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;30m²&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;240€&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;Service charges&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;per month&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;200€&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;Taxes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;IHK / BG / insurance&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;125€&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;Fees&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;20€/month&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;60€&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;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Total&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1297€&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;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Staff&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1600€&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;And if I were to hire someone to work in this store for 20hours/week, I would incur an estimated €1600 in costs.&lt;/p&gt;&#10;&lt;h3 id="summa-summarum"&gt;Summa summarum&lt;/h3&gt;&#10;&lt;p&gt;Ok, let&amp;rsquo;s summarize: I start with a mark-up of 20% on the production costs. As an assumed merchant, however, I would have to pay VAT, which I would have to add on again.&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;Retail price = production price &#10; + contribution margin (20%)&#10; + value added tax (19%)&#10; = (1,2 * 20,15€) * 1,19 = 28,77€&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Oh well, that puts us at almost thirty euros per item.&lt;/p&gt;&#10;&lt;p&gt;So how many soap butlers have to be sold per month before I would make a profit as a merchant with a store?&#10;The contribution margin is &lt;code&gt;4.03€&lt;/code&gt; (20% of the production price). If I now divide the costs generated by production and sales by this amount, I get my minimum sales quantity: &lt;code&gt;719 units/month&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;So how many hours per week would I have to spend at least at my CNC milling machine or assembling and packaging soap butlers?&#10;If I take the values from above, I get a total of &lt;code&gt;8 + 5 + 2 = 15min/part&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;With the minimum sales volume, that would give me a working week of about 45 hours!&#10;Oh, 60 hours a week is easy, isn&amp;rsquo;t it? So I could produce 960 units per month - and assuming they sell excellently, I would earn an extra &lt;code&gt;€971&lt;/code&gt; to amortize the development costs (&lt;code&gt;estimate 250h*€30 = €7,500&lt;/code&gt;). That&amp;rsquo;s a bang!&lt;/p&gt;&#10;&lt;p&gt;I would then march into the profit zone with the company after just eight months (with 60 hours per week, of course). Oh, or do you still have to pay taxes? And when was my machine paid off?&lt;/p&gt;&#10;&lt;h2 id="conclusion-on-profitability"&gt;Conclusion on profitability&lt;/h2&gt;&#10;&lt;p&gt;I realize that the calculation is not that simple. Some parameters cannot be scaled up or down arbitrarily and linearly and I cannot realistically estimate the machine costs, for example, because I simply don&amp;rsquo;t have any experience in continuous operation - keyword wear and tear.&lt;/p&gt;&#10;&lt;p&gt;Nevertheless, the amount of work I have to do is enough for me to say:&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;It won&amp;rsquo;t work. Nice project, but not worth it&lt;/strong&gt;, at least not with my own funds and on my own initiative. It remains a pilot series and my joy at a successful, functional product.&lt;/p&gt;&#10;&lt;p&gt;Fortunately, this is all &amp;ldquo;just my hobby&amp;rdquo;, so I&amp;rsquo;m not forced to draw the disastrous conclusions 😉&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;Careful. My vocation is not to be a trader or a businessman, but to be an engineer. That&amp;rsquo;s why I only have a very rudimentary knowledge of business administration (just that one semester at university when nobody really wanted to listen). All my comments here are rough estimates. They should therefore be taken with a grain of salt. However, I am very happy to receive corrections and constructive criticism!&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>'redir' through Caddyserver</title><link>https://blog.schallbert.de/en/migrating-to-subdomain/</link><pubDate>Tue, 30 Jan 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/migrating-to-subdomain/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-01-30-migrating-to-subdomain-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Thumbnail of correctly rendered &amp;#39;&amp;#39;website under construction&amp;#39;&amp;#39; page on schallbert.de"&#10; title="&amp;#39;redir&amp;#39; through Caddyserver" /&gt;&#10;&lt;p&gt;When I moved my website from Github Pages to &lt;a href="https://blog.schallbert.de/en/projects/move-blog-to-own-server/"&gt;my own server&lt;/a&gt;, I had some problems and unanswered questions regarding automatic redirects. My blog migrated from the root domain to a subdomain (blog.schallbert.de).&lt;/p&gt;&#10;&lt;h2 id="requirements-for-the-redirect"&gt;Requirements for the redirect&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Links already referencing to my blog articles under the old name, e.g. to &lt;a href="https://blog.schallbert.de/about/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;schallbert.de/about&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; should redirect correctly to the subdomain (no &lt;em&gt;404&lt;/em&gt;)&lt;/li&gt;&#10;&lt;li&gt;The root page under &lt;code&gt;schallbert.de&lt;/code&gt; should be fully usable&lt;/li&gt;&#10;&lt;li&gt;Data and files for domain and subdomains should be independent of each other&lt;/li&gt;&#10;&lt;li&gt;The web servers should be able to use different technologies for different subpages&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="implementation"&gt;Implementation&lt;/h2&gt;&#10;&lt;p&gt;At first I had thought that I could achieve the redirects in the DNS provider via &lt;code&gt;A&lt;/code&gt; and &lt;code&gt;CNAME&lt;/code&gt; entries. However, I quickly realized that &lt;em&gt;a)&lt;/em&gt; I had no idea how DNS even works and &lt;em&gt;b)&lt;/em&gt; the right way is via the configuration of my web server.&lt;/p&gt;&#10;&lt;h3 id="first-attempt-redir-at-"&gt;First attempt: redir at &amp;lsquo;/&amp;rsquo;&lt;/h3&gt;&#10;&lt;p&gt;On my web server (Caddy), redirects are specified in the &lt;code&gt;Caddyfile&lt;/code&gt; via &lt;code&gt;redir&lt;/code&gt;. This can be done &lt;code&gt;permanently&lt;/code&gt; (as so-called http &lt;code&gt;301&lt;/code&gt;) or &lt;code&gt;temporarily&lt;/code&gt; via &lt;code&gt;302&lt;/code&gt;. Default is a temporary redirect.&lt;/p&gt;&#10;&lt;p&gt;My first thought was to leave &lt;code&gt;schallbert.de&lt;/code&gt; unmodified and forward everything after a possible &lt;code&gt;/&lt;/code&gt;. This would then look like this in the Caddyfile:&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:#a6e22e"&gt;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:#75715e"&gt;# Define webserver&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/landing&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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Redir to subdomain if domain has a slash&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;@redirect path_regexp /*&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;redir @redirect https://blog.schallbert.de{uri}&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;As a result, Caddy simply forwarded everything to the subdomain. The catch was that the subdomain itself also contains slashes or a URI after the slash and is then forwarded yet again. Example:&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;schallbert.de/post -&amp;gt; blog.schallbert.de/post -&amp;gt; blog.schallbert.de/post -&amp;gt; blog.schallbert.de/post -&amp;gt; [...]&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;This gave me a recursive endless redirect. I completely paralyzed my server with it 😅&lt;/p&gt;&#10;&lt;h3 id="second-attempt-filtering-via-regexp"&gt;Second attempt: Filtering via regexp&lt;/h3&gt;&#10;&lt;p&gt;I need better filtering so that only slashes with additional characters behind them are actually redirected and the forwarding cannot be recursive. Fortunately, I had help from a good friend, so that the following &lt;code&gt;Caddyfile&lt;/code&gt; was created a short time later:&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:#a6e22e"&gt;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:#75715e"&gt;# Define webserver&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/landing&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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Redir to subdomain to maintain links for 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;@redirect path_regexp ^/[^/]+(/.*)?$&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;redir @redirect https://blog.schallbert.de{uri}&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;The Regexp checks whether there is a slash after the domain URL and whether it is followed by at least one other character. The redirection now works as desired and my blog is still fully accessible under the old links.&lt;/p&gt;&#10;&lt;p&gt;Nevertheless, I was still not completely satisfied. Because when I created a nice &amp;ldquo;landing page&amp;rdquo; on my root domain, suddenly only the plain HTML was visible. No favicon, no images, no CSS.&lt;/p&gt;&#10;&lt;p&gt;A quick look at the developer options quickly brought the realization that paths of all assets that I had stored on my landing page, marked with &lt;code&gt;/&lt;/code&gt; in the folder structure, were forwarded to the subdomain. Where they are not located, of course.&lt;/p&gt;&#10;&lt;h3 id="third-attempt-regexp-plus-file-search"&gt;Third attempt: Regexp plus file search&lt;/h3&gt;&#10;&lt;p&gt;Fortunately, I&amp;rsquo;m not alone with this problem, so I found the solution in &lt;a href="https://caddy.community/t/redirect-if-file-not-present/7902" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Caddy&amp;rsquo;s forum&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;:&lt;/p&gt;&#10;&lt;p&gt;With the &lt;code&gt;not file&lt;/code&gt; marker I can define redirects in &lt;code&gt;Caddyfile&lt;/code&gt; if resources on the current page cannot be found.&lt;/p&gt;&#10;&lt;p&gt;My solution now looks like this:&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:#a6e22e"&gt;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;@filenotfound {&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_regexp ^/[^/]+(/.*)?$&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;not 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;}&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;# show Caddy where to find page resources&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/landing&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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;route {&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;# Redirect to subdomain if article cannot found on 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;redir @filenotfound https://blog.schallbert.de{uri}&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;# else, define webserver and show page&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;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;Here it is important to define the page with &lt;code&gt;root * /www/&amp;lt;pagelocation&amp;gt;&lt;/code&gt; before creating the &lt;code&gt;route&lt;/code&gt;. Otherwise the assets will only become available after I have already forwarded them for lack of existing files.&lt;/p&gt;&#10;&lt;h2 id="success"&gt;Success!&lt;/h2&gt;&#10;&lt;p&gt;Now my construction site page is finally displayed correctly.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-01-30-migrating-to-subdomain.jpg" alt="Image: Correctly rendered &amp;#39;website under construction&amp;#39; page on schallbert.de"&gt;&lt;/figure&gt;&#10;</description></item><item><title>Automatic Upgrades for everything!</title><link>https://blog.schallbert.de/en/server-auto-upgrade/</link><pubDate>Sat, 20 Jan 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/server-auto-upgrade/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-01-20-server-auto-upgrade-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: unattended-upgrades and Watchtower keep my server and application packages up to date."&#10; title="Automatic Upgrades for everything!" /&gt;&#10;&lt;p&gt;As the title says, I would like to have security updates installed as soon as they are released. This applies to the Ubuntu on my cloud server as well as to all applications that I run in Docker. As I&amp;rsquo;m lazy, I don&amp;rsquo;t want to carry out these security updates manually.&lt;/p&gt;&#10;&lt;p&gt;For function updates and enhancements, however, it is still OK for me to intervene manually from time to time.&#10;Let&amp;rsquo;s start with the system upgrades.&lt;/p&gt;&#10;&lt;h2 id="security-updates-per-unattended-upgrades"&gt;Security updates per &lt;code&gt;unattended-upgrades&lt;/code&gt;&lt;/h2&gt;&#10;&lt;p&gt;Ubuntu supplies - as a short &lt;a href="https://askubuntu.com/questions/9/how-do-i-enable-automatic-updates" target="_blank" rel="noopener noreferrer" class="external-link"&gt;query reveals&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; - a configurable tool for automated upgrades called &lt;code&gt;unattended-upgrades&lt;/code&gt;. First, I check whether it is installed on my system:&lt;/p&gt;&#10;&lt;h3 id="installation"&gt;Installation&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert:~# which unattended-upgrades&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/usr/bin/unattended-upgrades&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Great. Otherwise I&amp;rsquo;d install it with &lt;code&gt;apt install unattended-upgrades&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="configure-unattended-upgrades"&gt;configure unattended-upgrades&lt;/h3&gt;&#10;&lt;p&gt;The &lt;a href="https://wiki.debian.org/UnattendedUpgrades" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Debian Wiki&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; to the rescue: Config files can be found at &lt;code&gt;/etc/apt/apt.conf.d&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;First I make sure that automatic upgrades are active. To do that I open &lt;code&gt;20auto-upgrades&lt;/code&gt; and check presence of following lines:&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;APT::Periodic::Update-Package-Lists &lt;span style="color:#e6db74"&gt;&amp;#34;1&amp;#34;&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;APT::Periodic::Unattended-Upgrade &lt;span style="color:#e6db74"&gt;&amp;#34;1&amp;#34;&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Very well, so automatic package index updates and -upgrades are active. Now, I should set an update time and upgrade types to install. These can be configured within&lt;code&gt;50unattended-upgrades&lt;/code&gt;, mostly by removing commented-out lines.&lt;/p&gt;&#10;&lt;h3 id="upgrade-sources"&gt;Upgrade sources&lt;/h3&gt;&#10;&lt;p&gt;The first section of the configuration file is dedicated to the permitted sources for the updates. Of course, only trustworthy providers should be listed here. My configuration includes updates for my Linux distribution and security plus &lt;code&gt;ESM&lt;/code&gt; = &amp;ldquo;Enhanced Security Maintenance&amp;rdquo; updates. It looks like this:&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;Unattended-Upgrade::Allowed-Origins &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:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;distro_id&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;distro_codename&lt;span style="color:#e6db74"&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:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;distro_id&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;distro_codename&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;-security&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;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;distro_id&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;ESMApps:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;distro_codename&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;-apps-security&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;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;distro_id&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;ESM:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;distro_codename&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;-infra-security&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;}&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="updates-for-dev-releases-too"&gt;Updates for dev releases, too?&lt;/h3&gt;&#10;&lt;p&gt;I have the value set to &lt;code&gt;auto&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Unattended-Upgrade::DevRelease &lt;span style="color:#e6db74"&gt;&amp;#34;auto&amp;#34;&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="automatic-reboot"&gt;Automatic reboot&lt;/h3&gt;&#10;&lt;p&gt;Some security updates (e.g. concerning the &lt;a href="https://en.wikipedia.org/wiki/Kernel_%28operating_system%29" target="_blank" rel="noopener noreferrer" class="external-link"&gt;kernel&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;) require the machine to be restarted. They will therefore only take effect if you have set the corresponding values in the file. &lt;code&gt;WithUsers&lt;/code&gt; is a matter of taste, because you are force-logged off if by chance an update has just been installed that requires a restart. To practically rule this out, I have entered a time under &lt;code&gt;Reboot-Time&lt;/code&gt; when I am certainly not working with the machine.&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;Unattended-Upgrade::Automatic-Reboot &lt;span style="color:#e6db74"&gt;&amp;#34;true&amp;#34;&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Unattended-Upgrade::Automatic-Reboot-WithUsers &lt;span style="color:#e6db74"&gt;&amp;#34;true&amp;#34;&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Unattended-Upgrade::Automatic-Reboot-Time &lt;span style="color:#e6db74"&gt;&amp;#34;03:45&amp;#34;&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Of course, automatic reboot has repercussions for all the programs I use. I therefore make sure in my &lt;code&gt;docker-compose.yml&lt;/code&gt; files that &lt;code&gt;always&lt;/code&gt; or &lt;code&gt;unless-stopped&lt;/code&gt; is really noted in &lt;code&gt;restart:&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="notifications"&gt;Notifications&lt;/h3&gt;&#10;&lt;p&gt;I haven&amp;rsquo;t switched on automatic mails or other push notifications for the time being, as I&amp;rsquo;m still in test mode and don&amp;rsquo;t really want to receive a lot more messages. As soon as I change this, there will definitely be an update here.&lt;/p&gt;&#10;&lt;h3 id="function-test"&gt;Function test&lt;/h3&gt;&#10;&lt;p&gt;To see if unattended-upgrades does what it is supposed to do, I take a look at the logs under &lt;code&gt;/var/log/unattended-upgrades&lt;/code&gt;.&#10;The file &lt;code&gt;unattended-upgrades.log&lt;/code&gt; contains the following, for example:&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-ruby" data-lang="ruby"&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:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;06&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;59&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;781&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Starting&lt;/span&gt; unattended upgrades script&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;06&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;59&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;781&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Allowed&lt;/span&gt; origins &lt;span style="color:#e6db74"&gt;are&lt;/span&gt;: o&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;Ubuntu&lt;/span&gt;,a&lt;span style="color:#f92672"&gt;=&lt;/span&gt;jammy, o&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;Ubuntu&lt;/span&gt;,a&lt;span style="color:#f92672"&gt;=&lt;/span&gt;jammy&lt;span style="color:#f92672"&gt;-&lt;/span&gt;security, o&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;UbuntuESMApps&lt;/span&gt;,a&lt;span style="color:#f92672"&gt;=&lt;/span&gt;jammy&lt;span style="color:#f92672"&gt;-&lt;/span&gt;apps&lt;span style="color:#f92672"&gt;-&lt;/span&gt;security, o&lt;span style="color:#f92672"&gt;&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:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;06&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;59&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;781&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Initial&lt;/span&gt; &lt;span style="color:#e6db74"&gt;blacklist&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;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;06&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;59&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;782&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Initial&lt;/span&gt; whitelist (&lt;span style="color:#f92672"&gt;not&lt;/span&gt; strict):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;06&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;14&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;33&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;956&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Packages&lt;/span&gt; that will be &lt;span style="color:#e6db74"&gt;upgraded&lt;/span&gt;: libc&lt;span style="color:#f92672"&gt;-&lt;/span&gt;bin libc&lt;span style="color:#f92672"&gt;-&lt;/span&gt;dev&lt;span style="color:#f92672"&gt;-&lt;/span&gt;bin libc&lt;span style="color:#f92672"&gt;-&lt;/span&gt;devtools libc6 libc6&lt;span style="color:#f92672"&gt;-&lt;/span&gt;dev locales python3&lt;span style="color:#f92672"&gt;-&lt;/span&gt;twisted&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;06&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;14&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;33&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;956&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Writing&lt;/span&gt; dpkg log to &lt;span style="color:#e6db74"&gt;/var/&lt;/span&gt;log&lt;span style="color:#f92672"&gt;/&lt;/span&gt;unattended&lt;span style="color:#f92672"&gt;-&lt;/span&gt;upgrades&lt;span style="color:#f92672"&gt;/&lt;/span&gt;unattended&lt;span style="color:#f92672"&gt;-&lt;/span&gt;upgrades&lt;span style="color:#f92672"&gt;-&lt;/span&gt;dpkg&lt;span style="color:#f92672"&gt;.&lt;/span&gt;log&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;06&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;14&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;54&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;578&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;All&lt;/span&gt; upgrades installed&#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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;However, the file &lt;code&gt;unattended-upgrades-shutdown.log&lt;/code&gt; is still empty. I will check at a later date whether everything works here too.&lt;/p&gt;&#10;&lt;h2 id="automate-container-updates-with-watchtower"&gt;Automate container updates with &lt;code&gt;Watchtower&lt;/code&gt;&lt;/h2&gt;&#10;&lt;p&gt;Now to the updates of my applications. A quick chat with a couple of admins revealed this:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;It is tedious to keep all programs up to date manually&lt;/li&gt;&#10;&lt;li&gt;For containerized applications, there are services that solve this task centrally&lt;/li&gt;&#10;&lt;li&gt;There have already been bad experiences when the latest release is referenced in &lt;code&gt;docker-compose.yml&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;With &lt;code&gt;image: &amp;lt;application&amp;gt;:latest&lt;/code&gt;, not all applications are stable or create problems with dependencies&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;One of these solutions is offered by &lt;a href="https://containrrr.dev/watchtower/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Watchtower&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, which again runs in a container.&lt;/p&gt;&#10;&lt;h3 id="installation-1"&gt;Installation&lt;/h3&gt;&#10;&lt;p&gt;As usual I create a &lt;code&gt;docker-compose.yml&lt;/code&gt; in a new folder at &lt;code&gt;opt/watchtower&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-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# copied from https://containrrr.dev/watchtower/notifications/&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;# watchtower/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;&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;watchtower&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;containrrr/watchtower&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;/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;Watchtower needs &lt;code&gt;docker.sock&lt;/code&gt; to pull the updates and apply them to the existing containers.&#10;Apart from that, I have done nothing else for the time being. No notifications (for the reasons mentioned above), no logs, nothing. I hope this setup stays functional and I don&amp;rsquo;t have to post an &lt;em&gt;update&lt;/em&gt; here anytime soon.&lt;/p&gt;&#10;&lt;aside class="update-box update-box--warn" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ⚠️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Update: (haha) new container source für watchtower&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2026-02-20T00:00:00Z"&gt;&#10; 2026-02-20&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; The Watchtower solution from &lt;em&gt;containrrr&lt;/em&gt; no longer works on my system. It seems to have been discontinued. So I&amp;rsquo;m changing the &lt;code&gt;docker-compose.yml&lt;/code&gt; to: &lt;code&gt;image: nickfedor/watchtower:latest&lt;/code&gt;. In the long run, however, passing the Docker socket to a container is &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html#rule-1-do-not-expose-the-docker-daemon-socket-even-to-the-containers" target="_blank" rel="noopener noreferrer" class="external-link"&gt;not a good idea at all&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. I&amp;rsquo;ve already made a note to find a less vulnerable solution using a script.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;p&gt;Further steps for a validation of Watchtower&amp;rsquo;s functions, for example, can be found &lt;a href="https://www.digitalocean.com/community/tutorials/how-to-automatically-update-docker-container-images-with-watchtower-on-ubuntu-22-04" target="_blank" rel="noopener noreferrer" class="external-link"&gt;in a tutorial by DigitalOcean&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="procedure"&gt;Procedure&lt;/h3&gt;&#10;&lt;p&gt;Watchtower checks the installed Docker images for updates on a daily basis &lt;a href="https://containrrr.dev/watchtower/introduction/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;according to the documentation&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. If there is a new image release, Watchtower sends a &lt;code&gt;SIGTERM&lt;/code&gt; signal to the containers to be updated, whereupon they shut down (&lt;a href="https://containrrr.dev/watchtower/stop-signals/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;source&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;). The containers are then started up again.&lt;/p&gt;&#10;&lt;h3 id="involuntary-functional-test"&gt;Involuntary functional test&lt;/h3&gt;&#10;&lt;p&gt;A few days after starting the container, I dialed into my server again and looked through some logs at random. I just wanted to check what was going on.&lt;/p&gt;&#10;&lt;p&gt;In the &lt;code&gt;fail2ban&lt;/code&gt; log files I suddenly saw pages and pages of entries at a certain point in time, &lt;code&gt;14:42:56&lt;/code&gt;. More than I had seen in sum for days before. When I scrolled up to the beginning of this chain of entries, I saw that fail2ban had probably stopped for a moment.&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt; 2024-01-26 14:42:50,817 &amp;lt;container_id&amp;gt; INFO Shutdown in progress...&#10; 2024-01-26 14:42:50,817 &amp;lt;container_id&amp;gt; INFO Observer stop ... try to end queue 5 seconds&#10; 2024-01-26 14:42:50,837 &amp;lt;daemon_id&amp;gt; INFO Observer stopped, 0 events remaining.&#10; 2024-01-26 14:42:50,878 &amp;lt;container_id&amp;gt; INFO Stopping all jails&#10; 2024-01-26 14:42:50,879 &amp;lt;container_id&amp;gt; INFO Removed logfile: &amp;#39;/var/log/auth.log&amp;#39;&#10; 2024-01-26 14:42:50,879 &amp;lt;container_id&amp;gt; INFO Removed logfile: &amp;#39;/var/log/caddy2/gitea_access.log&amp;#39;&#10; 2024-01-26 14:42:50,879 &amp;lt;container_id&amp;gt; INFO Removed logfile: &amp;#39;/var/log/caddy2/server_access.log&amp;#39;&#10; 2024-01-26 14:42:51,052 &amp;lt;daemon_id&amp;gt; NOTIC [sshd] Flush ticket(s) with iptables&#10; 2024-01-26 14:42:51,063 &amp;lt;container_id&amp;gt; INFO Jail &amp;#39;sshd&amp;#39; stopped&#10; 2024-01-26 14:42:51,137 &amp;lt;daemon_id&amp;gt; NOTIC [caddy-status] Flush ticket(s) with iptables-multiport&#10; 2024-01-26 14:42:51,137 &amp;lt;container_id&amp;gt; INFO Jail &amp;#39;caddy-status&amp;#39; stopped&#10; 2024-01-26 14:42:51,138 &amp;lt;container_id&amp;gt; INFO Connection to database closed.&#10; 2024-01-26 14:42:51,139 &amp;lt;container_id&amp;gt; INFO Exiting Fail2ban&#10; 2024-01-26 14:42:56,173 &amp;lt;new_container_id&amp;gt; INFO --------------------------------------------------&#10; 2024-01-26 14:42:56,173 &amp;lt;new_container_id&amp;gt; INFO Starting Fail2ban v1.0.2&#10; 2024-01-26 14:42:56,173 &amp;lt;new_container_id&amp;gt; INFO Observer start...&#10;[...]&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;All the log entries at the same time start at the end of the section shown above. A whole bunch of IP addresses are loaded there in preparation to be banned.&#10;I looked at the Ubuntu syslog, slightly worried:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;Jan 26 14:42:55 schallbert systemd[1]: docker-&amp;lt;ID&amp;gt;.scope: Deactivated successful&#10;Jan 26 14:42:55 schallbert systemd[1]: docker-&amp;lt;ID&amp;gt;.scope: Consumed 21:05 CPU time&#10;Jan 26 14:42:55 schallbert dockerd[690]: time=&amp;#34;2024-01-26T14:42:55.228607617Z&amp;#34; level=info msg=&amp;#34;ignoring event&amp;#34; &#10;Jan 26 14:42:55 schallbert containerd[639]: time=&amp;#34;2024-01-26T14:42:55.229864632Z&amp;#34; level=info msg=&amp;#34;shim disconnected&amp;#34; &#10;Jan 26 14:42:55 schallbert containerd[639]: time=&amp;#34;2024-01-26T14:42:55.230125409Z&amp;#34; level=warning msg=&amp;#34;cleaning up after shim disconnected&amp;#34; i&amp;gt;&#10;Jan 26 14:42:55 schallbert containerd[639]: time=&amp;#34;2024-01-26T14:42:55.230145478Z&amp;#34; level=info msg=&amp;#34;cleaning up dead shim&amp;#34;&#10;Jan 26 14:42:55 schallbert containerd[639]: time=&amp;#34;2024-01-26T14:42:55.240586557Z&amp;#34; level=warning msg=&amp;#34;cleanup warnings time=\&amp;#34;2024-01-26T14:&amp;gt;&#10;Jan 26 14:42:55 schallbert dockerd[690]: time=&amp;#34;2024-01-26T14:42:55.241839655Z&amp;#34; level=warning msg=&amp;#34;ShouldRestart failed&#10;Jan 26 14:42:55 schallbert containerd[639]: time=&amp;#34;2024-01-26T14:42:55.870912387Z&amp;#34; level=info msg=&amp;#34;loading plugin \&amp;#34;io.containerd.event.v1.p&amp;gt;&#10;Jan 26 14:42:55 schallbert containerd[639]: time=&amp;#34;2024-01-26T14:42:55.870997689Z&amp;#34; level=info msg=&amp;#34;loading plugin \&amp;#34;io.containerd.internal.v&amp;gt;&#10;Jan 26 14:42:55 schallbert containerd[639]: time=&amp;#34;2024-01-26T14:42:55.871007368Z&amp;#34; level=info msg=&amp;#34;loading plugin \&amp;#34;io.containerd.ttrpc.v1.t&amp;gt;&#10;Jan 26 14:42:55 schallbert containerd[639]: time=&amp;#34;2024-01-26T14:42:55.871150831Z&amp;#34; level=info msg=&amp;#34;starting signal loop&amp;#34; namespace=moby path&amp;gt;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Hm, &lt;code&gt;cleaning up dead shim&lt;/code&gt;. That sounds ominous. A quick &lt;a href="https://iximiuz.com/en/posts/implementing-container-runtime-shim/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;search&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; shows that the shim forms an intermediate layer between the container manager and the container itself and forwards the container&amp;rsquo;s input and output. Its runtime is linked to that of the container.&lt;/p&gt;&#10;&lt;p&gt;In other words, I saw a normal shutdown followed by a restart. Fortunately, no break-in!&#10;All that remains is to find out the reason for the restart of Fail2ban. This reminds me that I don&amp;rsquo;t even know when Watchtower runs the updates&amp;hellip;&lt;/p&gt;&#10;&lt;p&gt;So I quickly looked in the Watchtower logs with &lt;code&gt;docker container logs &amp;lt;container_id&amp;gt;&lt;/code&gt; and behold:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;time=&amp;#34;2024-01-26T14:42:47Z&amp;#34; level=info msg=&amp;#34;Found new lscr.io/linuxserver/fail2ban:latest image (9bda077d765e)&amp;#34;&#10;time=&amp;#34;2024-01-26T14:42:50Z&amp;#34; level=info msg=&amp;#34;Stopping /fail2ban (e0317a105d53) with SIGTERM&amp;#34;&#10;time=&amp;#34;2024-01-26T14:42:55Z&amp;#34; level=info msg=&amp;#34;Creating /fail2ban&amp;#34;&#10;time=&amp;#34;2024-01-26T14:42:55Z&amp;#34; level=info msg=&amp;#34;Session done&amp;#34; Failed=0 Scanned=6 Updated=1 notify=no&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now I have proof: Watchtower does what it is supposed to do. It upgrades containers to a new version as soon as updates become available. Great!&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;PS:&lt;/strong&gt; Since I haven&amp;rsquo;t configured Watchtower very much, I assume from the timestamps in the logs that the updates are run daily at the time Watchtower is started. I wonder whether I should switch to a certain version of the image instead of using &lt;code&gt;latest&lt;/code&gt;, though.&lt;/p&gt;&#10;</description></item><item><title>Better security for my server</title><link>https://blog.schallbert.de/en/server-protection/</link><pubDate>Fri, 12 Jan 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/server-protection/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-01-12-server-protection-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: side-by-side image of fail2ban and borgmatic logos"&#10; title="Better security for my server" /&gt;&#10;&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;&#10;&lt;p&gt;OK. I have my server setup, created an &lt;a href="https://blog.schallbert.de/en/gitea-action-runner-jekyll-dockerimage/"&gt;automatic build pipeline&lt;/a&gt; for my Blog and &lt;a href="https://blog.schallbert.de/en/self-hosted-jekyll-page-broken-links/"&gt;my website is displaying the Blog&lt;/a&gt; as it should. Still, I&amp;rsquo;m not completely done yet. Because I want to do more for server-side security than deactivating password logins.&lt;/p&gt;&#10;&lt;p&gt;And I don&amp;rsquo;t have backups. That&amp;rsquo;s never good, so let&amp;rsquo;s get something done about that.&lt;/p&gt;&#10;&lt;h2 id="locking-away-unwanted-guests"&gt;Locking away unwanted guests&lt;/h2&gt;&#10;&lt;p&gt;In my SSH-log of my machine&amp;rsquo;s root system, I get a lot of failed authentications:&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-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&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;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;19&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;29&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93842&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;Invalid&lt;/span&gt; user admin from &lt;span style="color:#ae81ff"&gt;41&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;207&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;248&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;204&lt;/span&gt; port &lt;span style="color:#ae81ff"&gt;37194&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;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;19&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;29&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93842&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: pam_unix(&lt;span style="color:#e6db74"&gt;sshd&lt;/span&gt;:auth): check pass; user unknown&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;19&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;29&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93842&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: pam_unix(&lt;span style="color:#e6db74"&gt;sshd&lt;/span&gt;:auth): authentication failure; logname&lt;span style="color:#f92672"&gt;=&lt;/span&gt; uid&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; euid&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; tty&lt;span style="color:#f92672"&gt;=&lt;/span&gt;ssh ru&lt;span style="color:#f92672"&gt;&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:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;19&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;31&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93842&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;Failed&lt;/span&gt; password &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; invalid user admin from &lt;span style="color:#ae81ff"&gt;41&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;207&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;248&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;204&lt;/span&gt; port &lt;span style="color:#ae81ff"&gt;37194&lt;/span&gt; ssh2&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;19&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;32&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93842&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;Connection&lt;/span&gt; closed by invalid user admin &lt;span style="color:#ae81ff"&gt;41&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;207&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;248&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;204&lt;/span&gt; port &lt;span style="color:#ae81ff"&gt;37194&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;preauth&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;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;14&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93886&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;Invalid&lt;/span&gt; user svn from &lt;span style="color:#ae81ff"&gt;84&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;108&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;40&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;27&lt;/span&gt; port &lt;span style="color:#ae81ff"&gt;44968&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;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;14&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93886&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: pam_unix(&lt;span style="color:#e6db74"&gt;sshd&lt;/span&gt;:auth): check pass; user unknown&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;14&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93886&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: pam_unix(&lt;span style="color:#e6db74"&gt;sshd&lt;/span&gt;:auth): authentication failure; logname&lt;span style="color:#f92672"&gt;=&lt;/span&gt; uid&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; euid&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; tty&lt;span style="color:#f92672"&gt;=&lt;/span&gt;ssh ru&lt;span style="color:#f92672"&gt;&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:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;16&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93886&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;Failed&lt;/span&gt; password &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; invalid user svn from &lt;span style="color:#ae81ff"&gt;84&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;108&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;40&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;27&lt;/span&gt; port &lt;span style="color:#ae81ff"&gt;44968&lt;/span&gt; ssh2&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;16&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93886&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;Received&lt;/span&gt; disconnect from &lt;span style="color:#ae81ff"&gt;84&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;108&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;40&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;27&lt;/span&gt; port &lt;span style="color:#ae81ff"&gt;44968&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;Bye&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Bye&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;preauth&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;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;16&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93886&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;Disconnected&lt;/span&gt; from invalid user svn &lt;span style="color:#ae81ff"&gt;84&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;108&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;40&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;27&lt;/span&gt; port &lt;span style="color:#ae81ff"&gt;44968&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;preauth&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:#f92672"&gt;[...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If I didn&amp;rsquo;t know that this is typical &amp;ldquo;internet noise&amp;rdquo;, I&amp;rsquo;d be nervous. Isn&amp;rsquo;t it coming close to thieves trying different keys at our front doors every few seconds? So what can we do about it? Ban them.&lt;/p&gt;&#10;&lt;h3 id="fail2ban"&gt;fail2ban&lt;/h3&gt;&#10;&lt;p&gt;That&amp;rsquo;s exactly what &lt;a href="https://github.com/fail2ban/fail2ban" target="_blank" rel="noopener noreferrer" class="external-link"&gt;fail2ban&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; can do for me.&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt; __ _ _ ___ _ &#10; / _|__ _(_) |_ ) |__ __ _ _ _ &#10; | _/ _` | | |/ /| &amp;#39;_ \/ _` | &amp;#39; \ &#10; |_| \__,_|_|_/___|_.__/\__,_|_||_|&#10; v1.1.0.dev1 20??/??/??&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In short, &lt;code&gt;fail2ban&lt;/code&gt; scans access/auth logs&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; for IP-addresses causing multiple failed authentications, and bans them when surpassing a user-defined threshold within a defined period of time.&lt;/p&gt;&#10;&lt;p&gt;How does fail2ban work? It modifies &lt;a href="https://en.wikipedia.org/wiki/Iptables" target="_blank" rel="noopener noreferrer" class="external-link"&gt;iptables&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, thus accesses packet filter rules (buzzword Firewall) on the network layer. So incoming requests of already blocked IP-addresses will not even reach my applications&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;h3 id="install-fail2ban"&gt;install fail2ban&lt;/h3&gt;&#10;&lt;p&gt;&lt;code&gt;fail2ban&lt;/code&gt; seems to be close to an industry standard for blocking unwanted access requests on Linux. Every hobbyist admin I know is using it.&lt;/p&gt;&#10;&lt;p&gt;For installation and configuration of fail2ban there&amp;rsquo;s a ton of guides out there plus the (well-written) on inside its Github-repository. I won&amp;rsquo;t be pressing the point here.&lt;/p&gt;&#10;&lt;p&gt;I chose an installation as Docker container to get all dependencies auto-delivered as well. I&amp;rsquo;m using the distribution by &lt;a href="https://docs.linuxserver.io/images/docker-fail2ban/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;linuxserver&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and write the following &lt;code&gt;docker-compose.yml&lt;/code&gt; to have it deployed:&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;# /fail2ban/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;2.1&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;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;fail2ban&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;lscr.io/linuxserver/fail2ban: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;fail2ban&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;cap_add&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;NET_ADMIN&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;NET_RAW&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;network_mode&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;host&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;PUID=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;PGID=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;TZ=Etc/UTC&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;VERBOSITY=-vv&lt;/span&gt; &lt;span style="color:#75715e"&gt;#optional&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;./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;/var/log/auth.log:/var/log/auth.log:ro&lt;/span&gt; &lt;span style="color:#75715e"&gt;# host ssh&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/log/caddy2:/var/log/caddy2:ro &lt;/span&gt; &lt;span style="color:#75715e"&gt;# gitea via caddy, caddyserver&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;What I add to add here: &lt;code&gt;fail2ban&lt;/code&gt; requires the Access logs as Volume (above added with &lt;code&gt;:ro&lt;/code&gt; as read-only volumes). A lot of filter rules come predefined in its &lt;code&gt;config&lt;/code&gt; folder, I just had to add a modified &lt;code&gt;jail.local&lt;/code&gt; &lt;a href="https://github.com/linuxserver/fail2ban-confs" target="_blank" rel="noopener noreferrer" class="external-link"&gt;inspiration: &lt;code&gt;linuxserver/fail2ban-confs&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and add a rule in &lt;code&gt;filter.d&lt;/code&gt; for caddy &lt;a href="https://muetsch.io/how-to-integrate-caddy-with-fail2ban.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;pulled from muetsch.io&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; to complete my door watch.&lt;/p&gt;&#10;&lt;h3 id="fail2ban-example"&gt;fail2ban example&lt;/h3&gt;&#10;&lt;p&gt;This is how a fail2ban-log looks like for my SSH-Daemon (sshd):&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-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# schallbert:/opt/fail2ban/config/log/fail2ban# grep &amp;#34;220.124.89.47&amp;#34; fail2ban.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;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;600&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;FBB3630BB38&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;sshd&lt;span style="color:#f92672"&gt;]&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Found&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;220&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;124&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;89&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;47&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;20&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;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;23&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;003&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;FBB3630BB38&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;sshd&lt;span style="color:#f92672"&gt;]&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Found&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;220&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;124&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;89&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;47&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&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:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;25&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;205&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;FBB3630BB38&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;sshd&lt;span style="color:#f92672"&gt;]&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Found&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;220&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;124&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;89&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;47&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;24&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;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;27&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;206&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;FBB3630BB38&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;sshd&lt;span style="color:#f92672"&gt;]&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Found&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;220&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;124&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;89&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;47&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;26&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;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;32&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;610&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;FBB3630BB38&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;sshd&lt;span style="color:#f92672"&gt;]&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Found&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;220&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;124&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;89&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;47&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;32&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;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;33&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;045&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;FBB36104B38&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NOTIC&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;sshd&lt;span style="color:#f92672"&gt;]&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Ban&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;220&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;124&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;89&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;47&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;kbye!&lt;/p&gt;&#10;&lt;h3 id="what-does-not-yet-work-gitea--fail2ban"&gt;What does not (yet) work: Gitea &amp;amp; fail2ban&lt;/h3&gt;&#10;&lt;p&gt;Also on my gitea instance, I get quite some authentication requests that I&amp;rsquo;d like to block. Unfortunately, I cannot get Gitea to put the registered accesses into a log file. I see them in the console only. On the other hand, the shell access to Gitea is already secured as it is channeled through &lt;code&gt;sshd&lt;/code&gt; that also guarantees remote access to my server.&lt;/p&gt;&#10;&lt;p&gt;What is missing is a ban for failed web login requests.&lt;/p&gt;&#10;&lt;p&gt;I thought I had configured &lt;code&gt;app.ini&lt;/code&gt; to get access logs written to file:&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;# gitea/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:#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:#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;warn&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;&lt;span style="color:#a6e22e"&gt;ENABLE_ACCESS_LOGS&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_SSH_LOG&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;logger.access.MODE&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;access-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;[log.access-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;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;ACCESS&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;FILE_NAME&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;access.log&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;[...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I have both activated the logs with &lt;code&gt;ENABLE_ACCESS_LOGS&lt;/code&gt; and configured log-to-file. &lt;code&gt;access.log&lt;/code&gt; is being generated but no authentication tries are being written to it. Maybe caddy&amp;rsquo;s reverse proxy snitches them away before they reach Gitea? I&amp;rsquo;m sure I&amp;rsquo;ll find out at a later point in time.&lt;/p&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; testing fail2ban with ipdables and banip&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2024-08-22T00:00:00Z"&gt;&#10; 2024-08-22&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; At some point I asked myself whether &lt;em&gt;fail2ban&lt;/em&gt; actually works on the iptables of my server or only operates within the container. Luckily, I was &lt;a href="https://stackoverflow.com/questions/59996070/how-to-understand-if-the-fail2ban-ssh-filter-is-working-with-a-new-port" target="_blank" rel="noopener noreferrer" class="external-link"&gt;not the first with this question (stackoverflow)&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and found the proposed solution quite charming:&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;p&gt;At some point I asked myself whether &lt;em&gt;fail2ban&lt;/em&gt; actually works on the iptables of my server or only operates within the container. Luckily, I was &lt;a href="https://stackoverflow.com/questions/59996070/how-to-understand-if-the-fail2ban-ssh-filter-is-working-with-a-new-port" target="_blank" rel="noopener noreferrer" class="external-link"&gt;not the first with this question (stackoverflow)&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and found the proposed solution quite charming:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Block any IP in the Docker container (&lt;code&gt;docker exec -it fail2ban sh&lt;/code&gt;): &lt;code&gt;fail2ban-client set sshd banip 111.111.111.111&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Check the iptables to see if this IP appears there: &lt;code&gt;iptables -n -L --line-numbers&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;In my case: enjoy, because it is there: &lt;code&gt;1 REJECT all -- 111.111.111.111&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Unblock the IP again &lt;code&gt;fail2ban-client set sshd unbanip 111.111.111.111&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Using fail2ban to protect more than just ssh&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2024-08-22T00:00:00Z"&gt;&#10; 2024-08-22&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; I have now found a solution for &lt;em&gt;Gitea&lt;/em&gt;, as well as all my other websites:&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Protection against brute-force attacks via &lt;code&gt;ssh&lt;/code&gt; as described above&lt;/li&gt;&#10;&lt;li&gt;Protection against overload using a &lt;a href="https://blog.schallbert.de/en/fail2ban-with-caddy/"&gt;Rate Limiter&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Protection against attacks on the APIs (&lt;code&gt;404/403&lt;/code&gt; attacks) using &lt;code&gt;caddy-status&lt;/code&gt; configuration for fail2ban, logs in &lt;code&gt;JSON&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&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;# /config/fail2ban/filter.d/caddy-status.conf&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;# this regex works for caddy with json-style logs&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;failregex = &amp;#34;client_ip&amp;#34;:&amp;#34;&amp;lt;HOST&amp;gt;&amp;#34;(.*)&amp;#34;status&amp;#34;:(400|401|403|404|500)&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;datepattern = \d+&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;ignoreregex =&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="regular-backups"&gt;Regular backups&lt;/h2&gt;&#10;&lt;p&gt;I want to be able to do disaster recovery. Unforeseen events impacting my server like incompatible updates, machine downtime or security breaches should not keep me from spawning and configuring a new, healthy instance in no time.&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;m not willing to create backups manually, copy and compress folders, finally downloading to a backup location via &lt;code&gt;SCP / SFTP&lt;/code&gt;. This should take place automatically, and, ideally, without cost. A quick search reveals two suitable open-source tools: &lt;a href="https://torsion.org/borgmatic/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;borgmatic&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and &lt;a href="https://restic.net/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;restic&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;I randomly chose Borgmatic.&lt;/p&gt;&#10;&lt;h3 id="install-borgmatic-with-docker"&gt;install Borgmatic with Docker&lt;/h3&gt;&#10;&lt;p&gt;As for all other components so far, I want Borgmatic to run in Docker environment. Luckily there are &lt;a href="https://hub.docker.com/r/b3vis/borgmatic/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ready-made solutions&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; that need just a little configuration.&lt;/p&gt;&#10;&lt;p&gt;This is how my &lt;code&gt;docker-compose&lt;/code&gt; file looks like:&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;# /borgmatic/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;#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;borgmatic&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;ghcr.io/borgmatic-collective/borgmatic&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;borgmatic&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;${VOLUME_SOURCE}:/mnt/source:ro &lt;/span&gt; &lt;span style="color:#75715e"&gt;# backup source&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;${VOLUME_TARGET}:/mnt/repository &lt;/span&gt; &lt;span style="color:#75715e"&gt;# backup target&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;${VOLUME_ETC_BORGMATIC}:/etc/borgmatic.d/ &lt;/span&gt; &lt;span style="color:#75715e"&gt;# borgmatic config file(s) + crontab.txt&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;${VOLUME_BORG_CONFIG}:/root/.config/borg &lt;/span&gt; &lt;span style="color:#75715e"&gt;# config and keyfiles&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;${VOLUME_SSH}:/root/.ssh &lt;/span&gt; &lt;span style="color:#75715e"&gt;# ssh key for remote repositories&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;${VOLUME_BORG_CACHE}:/root/.cache/borg &lt;/span&gt; &lt;span style="color:#75715e"&gt;# checksums used for deduplication&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;# - /var/run/docker.sock:/var/run/docker.sock # add docker sock so borgmatic can start/stop containers to be backupped&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;TZ=${TZ}&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;BORG_PASSPHRASE=${BORG_PASSPHRASE}&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Inspirations were the related &lt;a href="https://github.com/borgmatic-collective/docker-borgmatic/blob/master/README.md" target="_blank" rel="noopener noreferrer" class="external-link"&gt;docs on Github&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. You can see that Borgmatic requires a lot of volumes to work properly, most importantly backup source and target location.&lt;/p&gt;&#10;&lt;h3 id="configure-borgmatic"&gt;configure Borgmatic&lt;/h3&gt;&#10;&lt;p&gt;All concrete data behind &lt;code&gt;${}&lt;/code&gt; are summarized in an &lt;code&gt;.env&lt;/code&gt; environment variable file. This helps me avoid publishing passphrases by accident. Of course I have noted down the passphrase on paper as &amp;ldquo;backup&amp;rdquo; of the backup.&lt;/p&gt;&#10;&lt;p&gt;I then modify &lt;code&gt;config.yml&lt;/code&gt; in &lt;code&gt;borgmatic.d/&lt;/code&gt; slightly, adding backup source, target, count, and cycle. That&amp;rsquo;s all, I&amp;rsquo;m ready for a first test.&lt;/p&gt;&#10;&lt;h3 id="testing-borgmatic-backup"&gt;testing Borgmatic backup&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker exec borgmatic bash -c &lt;span style="color:#ae81ff"&gt;\&#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;&amp;#34;cd &amp;amp;&amp;amp; borgmatic --stats -v 1 --files 2&amp;gt;&amp;amp;1&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This command has &lt;code&gt;borgmatic&lt;/code&gt; create a backup for me through the Docker container.&lt;/p&gt;&#10;&lt;p&gt;I got an error message right away, saying &lt;code&gt;repository does not exist&lt;/code&gt;. A look into the docs reveals that I have to manually create the backup target repository.&lt;/p&gt;&#10;&lt;p&gt;The following command does this, creating an empty, encrypted repo:&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 exec borgmatic bash -c &lt;span style="color:#ae81ff"&gt;\&#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;&amp;#34;borgmatic init --encryption repokey-blake2&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now I retry manual backup creation which throws no error&#10;.&lt;/p&gt;&#10;&lt;h3 id="ein-backup-only-is-a-backup"&gt;Ein Backup only is a Backup&amp;hellip;&lt;/h3&gt;&#10;&lt;p&gt;&amp;hellip;when it is successfully applied, a friend of mine said.&lt;/p&gt;&#10;&lt;p&gt;Right he is. Still, I don&amp;rsquo;t dare replacing my working server configuration with a backup. As a compromise, I go halfway by creating a &lt;code&gt;docker-compose.restore.yml&lt;/code&gt; following the &lt;a href="https://github.com/borgmatic-collective/docker-borgmatic/blob/master/docker-compose.restore.yml" target="_blank" rel="noopener noreferrer" class="external-link"&gt;online guide&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. For additional help, I read &lt;a href="https://www.modem7.com/books/docker-backup/page/backup-docker-using-borgmatic" target="_blank" rel="noopener noreferrer" class="external-link"&gt;modem7&amp;rsquo;s related blog post&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;For my test purposes, I run &lt;code&gt;docker-compose.restore.yml&lt;/code&gt; in the container&amp;rsquo;s shell and enter the following commands:&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;mkdir backuprestoremount&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;borg mount /mnt/repository /backuprestoremount&#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;mkdir backuprestore&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;borgmatic extract --archive latest --destination /backuprestore&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here, I create the folder &lt;code&gt;/backuprestoremount&lt;/code&gt; and have it point to my borg backup. Then, I extract the backup into &lt;code&gt;/backuprestore&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Let&amp;rsquo;s do a sanity check:&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;&lt;span style="color:#75715e"&gt;# cd /mnt/backuprestore&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;# /restore/mnt/source ls&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;borgmatic caddy2 containerd fail2ban gitea hostedtoolcache watchtower&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Yeah! The backup contains all applications along with their files. I stop at this point as I&amp;rsquo;m too lazy and cowardly to overwrite my working setup as mentioned above.&lt;/p&gt;&#10;&lt;h3 id="what-yet-doesnt-work-borgmatic--docker-compose-down"&gt;What (yet) doesn&amp;rsquo;t work: Borgmatic &amp;amp; docker-compose down&lt;/h3&gt;&#10;&lt;p&gt;Borgmatic offers a simple method to run pre- and post-backup actions. As I want to maintain consistency within my containers, I want them to be shutdown before and restarted after the backup run. So I wrote a script, hooked it into the config and&amp;hellip; nothing.&lt;/p&gt;&#10;&lt;p&gt;Easy to follow, this can only work directly if Borgmatic ran on bare metal. Docker&amp;rsquo;s encapsulation doesn&amp;rsquo;t let borgmatic run shell scripts outside its container. I thought I could circumvent this problem by including &lt;code&gt;var/run/docker.sock&lt;/code&gt; in the docker-compose file but without success.&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;m sure this can be solved somehow. For the time being, I have more important problems to solve and thus I&amp;rsquo;ll live with the assumption &amp;ldquo;there are no data inconsistencies&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;.&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;The SSH-Daemon of my server puts its authentication logs to &lt;code&gt;/var/log/auth.log&lt;/code&gt;, but could also be named &lt;code&gt;access.log&lt;/code&gt; or similar in other applications. Services like caddy or gitea also create auth log files.&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;However, what I yet do not understand is how &lt;code&gt;fail2ban&lt;/code&gt; is able to access the iptables from within the container. I thought a positive side effect of containerization is encapsulation. Or is that only complete with &amp;ldquo;rootless&amp;rdquo;-Containers?&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;Background: I&amp;rsquo;m currently the only one contributing to my &lt;code&gt;gitea&lt;/code&gt; instance. My website is static. The webserver &lt;code&gt;caddy&lt;/code&gt; only handles changing files once pushed to Gitea. I have scheduled application updates per &lt;code&gt;watchtower&lt;/code&gt; and &lt;code&gt;unattended-upgrades&lt;/code&gt; &lt;a href="https://blog.schallbert.de/en/server-auto-upgrade/"&gt;(see my follow-up post)&lt;/a&gt; at night so they won&amp;rsquo;t interfere with &lt;code&gt;borgmatic&lt;/code&gt; runs. Just the logs for &lt;code&gt;fail2ban&lt;/code&gt; are still written independently. I accept the risk of data loss here because packet filtering is subject to change anyways.&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>Jekyll build: broken relative links</title><link>https://blog.schallbert.de/en/self-hosted-jekyll-page-broken-links/</link><pubDate>Fri, 29 Dec 2023</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/self-hosted-jekyll-page-broken-links/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-12-29_serves_broken_links-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Webpage, displaying Schallbert&amp;#39;&amp;#39;s Blog with broken links"&#10; title="Jekyll build: broken relative links" /&gt;&#10;&lt;h2 id="problem"&gt;Problem&lt;/h2&gt;&#10;&lt;p&gt;Now, after &lt;a href="https://blog.schallbert.de/en/gitea-action-runner-jekyll-dockerimage/"&gt;eternal trial and error&lt;/a&gt;, I am finally able to provide my Caddy server with the build files via Docker-Volume. Unfortunately, the page looks like this:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-12-29_serves_broken_links.jpg" alt="Image: Broken webpage, displaying Schallbert&amp;#39;s Blog without pictures and other media, and any links lead to 404-nowhere"&gt;&lt;/figure&gt;&#10;&lt;h2 id="analysis"&gt;Analysis&lt;/h2&gt;&#10;&lt;p&gt;At first I suspected the web server.&#10;But when I call up the website in my browser&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; as a test, I see the same behavior. Strange.&lt;/p&gt;&#10;&lt;p&gt;Obviously all stylesheets are missing. But if you look closely, images and other media have not been loaded either. Only their descriptions.&#10;Then I click wildly on a few links to articles and subpages. I notice that the paths look different than I would have expected:&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;# --- Expectation ---&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;# Image&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;/assets/images/test.jpg&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;# Post&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;/this-is-a-post&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Instead I see:&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;# --- Observation ---&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;# Image&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;/pages/&amp;lt;username&amp;gt;/&amp;lt;repositoryname&amp;gt;/assets/images/test.jpg&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;# Post&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;/pages/&amp;lt;username&amp;gt;/&amp;lt;repositoryname&amp;gt;/this-is-a-post&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is also shown by the browser&amp;rsquo;s developer options activated with &lt;code&gt;F12&lt;/code&gt; (see image above): all sources integrated via a relative link cannot be loaded. It is generally a clear recommendation to have the developer options activated in the event of problems with the display of websites.&lt;/p&gt;&#10;&lt;p&gt;It is somehow obvious that my Jekyll configuration file &lt;code&gt;_config.yml&lt;/code&gt; or my &lt;a href="https://blog.schallbert.de/en/jekyll-polyglot-language-support/"&gt;translation header&lt;/a&gt; &lt;code&gt;i10.yml&lt;/code&gt; contain errors. Fortunately, there are already excellent &lt;a href="https://mademistakes.com/mastering-jekyll/site-url-baseurl/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;help pages&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;However, a test quickly shows that I cannot leave the attribute &lt;code&gt;repository&lt;/code&gt; empty, where the build &lt;code&gt;&amp;lt;username&amp;gt;/&amp;lt;repositoryname&amp;gt;&lt;/code&gt; emanates from:&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;&lt;span style="color:#75715e"&gt;# bundle exec jekyll serve&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Generating... &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Jekyll Feed: Generating feed &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; posts&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Liquid Exception: No repo name found. &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Specify using PAGES_REPO_NWO environment variables, &lt;span style="color:#e6db74"&gt;&amp;#39;repository&amp;#39;&lt;/span&gt; in your configuration, &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; or set up an &lt;span style="color:#e6db74"&gt;&amp;#39;origin&amp;#39;&lt;/span&gt; git remote pointing to your github.com repository. in /_layouts/default.html&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ERROR: YOUR SITE COULD NOT BE BUILT&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And, heck, where does the &lt;code&gt;/pages&lt;/code&gt; suddenly come from?&lt;/p&gt;&#10;&lt;h2 id="solution"&gt;Solution&lt;/h2&gt;&#10;&lt;p&gt;As &lt;code&gt;/pages&lt;/code&gt; is added to paths I suspect &lt;a href="https://jekyllrb.com/docs/configuration/options/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Jekyll build options&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and &lt;a href="https://github.com/github/pages-gem" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Github-Pages Gem&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; to be the culprits.&lt;/p&gt;&#10;&lt;p&gt;Of course I&amp;rsquo;m not alone with this problem, so I found the &lt;a href="https://stackoverflow.com/questions/51869314/jekyll-serve-generate-wrong-path-in-localhost" target="_blank" rel="noopener noreferrer" class="external-link"&gt;solution documented&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;I just change &lt;code&gt;JEKYLL_ENV&lt;/code&gt; (build environment) in the action script from &lt;code&gt;production&lt;/code&gt; to &lt;code&gt;development&lt;/code&gt;, like it is normally done for building on a local machine.&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;# workflows/jekyll-build-action.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;env&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;JEKYLL_ENV&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;development&lt;/span&gt; &lt;span style="color:#75715e"&gt;# I had &amp;#34;production&amp;#34; here before&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And what can I say: as soon as you do it right, it works!&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-12-29_css_assets_show_correctly.jpg" alt="Image: blog.schallbert.de shows correct CSS and media are loaded along working links"&gt;&lt;/figure&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;Displaying the website offline works as follows: Drag the built files (the &lt;code&gt;_site&lt;/code&gt; folder) to your own computer and open the top-level &lt;code&gt;index.html&lt;/code&gt; with the browser.&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>🗸 Gitea Actions Part2 - Jekyll-Dockerimage</title><link>https://blog.schallbert.de/en/gitea-action-runner-jekyll-dockerimage/</link><pubDate>Sat, 09 Dec 2023</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/gitea-action-runner-jekyll-dockerimage/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-12-09_gitea_infrastructure-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: giteas infrastructure in a nutshell"&#10; title="🗸 Gitea Actions Part2 - Jekyll-Dockerimage" /&gt;&#10;&lt;aside class="update-box update-box--warn" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ⚠️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Gitea Retires `act_runner`&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2026-09-15T00:00:00Z"&gt;&#10; 2026-09-15&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; This article refers to an Actions implementation by Gitea, the &lt;code&gt;act_runner&lt;/code&gt;. It is derived from &lt;a href="https://github.com/nektos/act" target="_blank" rel="noopener noreferrer" class="external-link"&gt;nectos/act&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Gitea now uses &lt;a href="https://blog.gitea.com/release-of-runner-1.0.0/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;its own runner&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. The old runner should be replaced. More info: Read my post to &lt;a href="https://blog.schallbert.de/en/build-deploy-hugo-with-actions-docker-caddy/"&gt;deploy hugo with Gitea Actions, docker, and caddy&lt;/a&gt;&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;h2 id="what-happened-so-far"&gt;What happened so far&lt;/h2&gt;&#10;&lt;p&gt;I wasn&amp;rsquo;t able to set up the CI/CD pipeline in &lt;a href="https://blog.schallbert.de/en/gitea-action-runner-native-jekyll/"&gt;Part1&lt;/a&gt; of this sequel. I tried with a minor modified copy of the Github-Action to build Jekyll for Github Pages. Now I wanted to try running Jekyll as a Dockerimage right from the start. There wasn&amp;rsquo;t too much choice on the Docker hub, still I found something which seemed well-documented and actively maintained at year-end 2023.&lt;/p&gt;&#10;&lt;h2 id="set-up-job"&gt;&amp;ldquo;Set up job&amp;rdquo;&lt;/h2&gt;&#10;&lt;p&gt;So I add the Docker-jekyllimag to my workflow:&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;# workflows/jekyll-build-action.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;jobs&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;# Build job&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;build&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;runs-on&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ubuntu-latest&lt;/span&gt; &lt;span style="color:#75715e"&gt;# this is the &amp;#34;label&amp;#34; the runner will use and map to docker target OS&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&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jvconseil/jekyll-docker&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here I&amp;rsquo;m telling &lt;code&gt;gitea act_runner&lt;/code&gt; to run with &lt;code&gt;ubuntu-latest&lt;/code&gt; label which in my case points to a minimalistic &lt;code&gt;node16:bullseye&lt;/code&gt; machine (Debian11). I&amp;rsquo;m then telling docker to load the &lt;code&gt;jekyll-docker&lt;/code&gt; image, where the dependencies I require for my build are already loaded.&lt;/p&gt;&#10;&lt;h2 id="bundle-install"&gt;bundle install&lt;/h2&gt;&#10;&lt;p&gt;Well, initial build fails due to ruby dependencies that are specified in my project&amp;rsquo;s &lt;code&gt;Gemfile&lt;/code&gt; which are not present yet. The error message looks like this:&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;bundler: failed to load command: jekyll &lt;span style="color:#f92672"&gt;(&lt;/span&gt;/usr/gem/bin/jekyll&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;/usr/local/lib/ruby/gems/3.2.0/gems/bundler-2.4.22/lib/bundler/resolver.rb:332:in &lt;span style="color:#e6db74"&gt;`&lt;/span&gt;raise_not_found!&lt;span style="color:#e6db74"&gt;&amp;#39;: Could not find gem &amp;#39;&lt;/span&gt;github-pages&lt;span style="color:#e6db74"&gt;&amp;#39; in locally installed gems. (Bundler::GemNotFound)&#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;&#9;from /usr/local/lib/ruby/gems/3.2.0/gems/bundler-2.4.22/lib/bundler/resolver.rb:392:in `block in prepare_dependencies&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;a href="https://bundler.io" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Bundler&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; is Ruby&amp;rsquo;s packet manager. Similar to &lt;code&gt;npm&lt;/code&gt; for Javascript or &lt;code&gt;pip&lt;/code&gt; für Python, &lt;code&gt;bundler&lt;/code&gt; is able to load and bind dependencies and libraries for applications running in Ruby. The &lt;code&gt;Gemfile&lt;/code&gt; specifies the packages for &lt;code&gt;bundler&lt;/code&gt; to install.&lt;/p&gt;&#10;&lt;p&gt;So let&amp;rsquo;s add &lt;code&gt;bundle install&lt;/code&gt; to the script like so:&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;# workflows/jekyll-build-action.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;steps&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;CHECKOUT ---&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;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/checkout@v3&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;INSTALL GEMS ---&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;run&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;bundle install&lt;/span&gt; &lt;span style="color:#75715e"&gt;# will fail with permissions rights to write to Gemfile.lock but anyways installs required dependencies.&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;BUILD WITH JEKYLL ---&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;# Outputs to the &amp;#39;./_site&amp;#39; directory by 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;run&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;bundle exec jekyll build --destination /opt/blog_staging&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;env&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;JEKYLL_ENV&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;production&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="act_runner-no-write-permissions-to-gemfilelock"&gt;act_runner: no write permissions to &lt;code&gt;Gemfile.lock&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;This fails again. &lt;code&gt;act_runner&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-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 | &lt;span style="color:#f92672"&gt;[&lt;/span&gt;Deploy Jekyll site/build&lt;span style="color:#f92672"&gt;]&lt;/span&gt; | &lt;span style="color:#e6db74"&gt;`&lt;/span&gt;/workspace/schallbert/blog/Gemfile.lock&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;. It is likely that you need to grant&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 | &lt;span style="color:#f92672"&gt;[&lt;/span&gt;Deploy Jekyll site/build&lt;span style="color:#f92672"&gt;]&lt;/span&gt; | write permissions &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; that path.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 | &lt;span style="color:#f92672"&gt;[&lt;/span&gt;Deploy Jekyll site/build&lt;span style="color:#f92672"&gt;]&lt;/span&gt; ❌ Failure - Main ---INSTALL GEMS ---&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 | &lt;span style="color:#f92672"&gt;[&lt;/span&gt;Deploy Jekyll site/build&lt;span style="color:#f92672"&gt;]&lt;/span&gt; exitcode &lt;span style="color:#e6db74"&gt;&amp;#39;23&amp;#39;&lt;/span&gt;: failure&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After a lenghty search I saw that &lt;code&gt;Gemfile.lock&lt;/code&gt; wasn&amp;rsquo;t added to version control on my local machine because it is mentioned in &lt;code&gt;.gitignore&lt;/code&gt;. So bundler in &lt;code&gt;act_runner&lt;/code&gt; now tries to create it out of the given &lt;code&gt;Gemfile&lt;/code&gt; without success as it only has read access to the checked-out dataset.&lt;/p&gt;&#10;&lt;p&gt;To solve this, I add &lt;code&gt;Gemfile.lock&lt;/code&gt; to version control, removing it from &lt;code&gt;.gitignore&lt;/code&gt;. As an additional benefit I make sure to have identical build environments locally and remote.&lt;/p&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Fixing bundler issues&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2025-11-04T00:00:00Z"&gt;&#10; 2025-11-04&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; After updating my local build environment via &lt;code&gt;bundle update&lt;/code&gt;, I&amp;rsquo;m experiencing the aforementioned problem again, even though I&amp;rsquo;m now versioning &lt;code&gt;Gemfile.lock&lt;/code&gt;. I discuss the solution in an &lt;a href="https://blog.schallbert.de/en/bundler-ci-gemfile-issue/"&gt;article on the topic of Bundler&lt;/a&gt;.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;h3 id="dependency-sass-embedded"&gt;Dependency: &lt;code&gt;sass-embedded&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;Less than 20sec into the build I get the following error:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;Resolving dependencies...&#10;Could not find gem &amp;#39;sass-embedded (= 1.69.5)&amp;#39; with platform &amp;#39;x86_64-linux&amp;#39; in&#10;rubygems repository https://rubygems.org/ or installed locally.&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After some more searching, I found a &lt;a href="https://github.com/helaili/jekyll-action/issues/150#issuecomment-1374388728" target="_blank" rel="noopener noreferrer" class="external-link"&gt;resolution&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;: Specify a certain version of &lt;code&gt;jekyll-sass-converter&lt;/code&gt; in the action or, alternatively, add the gem &amp;ldquo;github-pages&amp;rdquo; to the Gemfile. The latter manages to load a working version of CascadingStyleSheets (CSS) Preprocessor &lt;a href="https://sass-lang.com/guide/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Sass&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Finally, &lt;code&gt;bundle install&lt;/code&gt; finishes successfully:&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;✅ Bundle complete! &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt; Gemfile dependencies, &lt;span style="color:#ae81ff"&gt;43&lt;/span&gt; gems now installed.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="jekyll-build"&gt;jekyll build&lt;/h2&gt;&#10;&lt;p&gt;As I want &lt;code&gt;act_runner&lt;/code&gt; to save the build output on my host machine, I add a &lt;a href="https://docs.docker.com/storage/volumes/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Volume&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; to Gitea&amp;rsquo;s &lt;code&gt;docker-compose.yml&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-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;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/blog_staging:/opt/blog_staging&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;Let&amp;rsquo;s retry the build:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt; Destination: /opt/blog_staging&#10; Generating... &#10; Jekyll Feed: Generating feed for posts&#10;jekyll 3.9.3 | Error: Permission denied @ dir_s_mkdir - /opt/blog_staging&#10;/usr/local/lib/ruby/3.2.0/fileutils.rb:406:in `mkdir&amp;#39;: Permission denied @ dir_s_mkdir - /opt/blog_staging (Errno::EACCES)&#10;&#9;from /usr/local/lib/ruby/3.2.0/fileutils.rb:406:in `fu_mkdir&amp;#39;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Also this problem can easily be solved: Either you build to &lt;code&gt;/tmp&lt;/code&gt;, where the &lt;code&gt;jekyll&lt;/code&gt; user has access. Or you hand over the output directory like so: &lt;code&gt;chown -R jekyll /your/build/output&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;With this change, I get successful action runs.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-12-09_buildsuccessful.jpg" alt="Image: Gitea snapshot showing a successful build"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;h2 id="transfer-artifacts"&gt;Transfer artifacts&lt;/h2&gt;&#10;&lt;p&gt;Where do I find the build artifacts now? I don&amp;rsquo;t see any &lt;code&gt;_site&lt;/code&gt; folder: Neither on the host machine&amp;rsquo;s volume, nor in the Gitea or Runner containers.&lt;/p&gt;&#10;&lt;h3 id="docker-volumes-for-act_runner-cannot-share-build-artifact"&gt;Docker volumes for &lt;code&gt;act_runner&lt;/code&gt; cannot share build artifact&lt;/h3&gt;&#10;&lt;p&gt;This seems logical as I learn the mechanics of &lt;code&gt;act-runner&lt;/code&gt;: It will spawn an own action Dockerimage with own volumes that are associated by Task-ID.&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;DRIVER VOLUME NAME&#10;--&amp;gt; local GITEA-ACTIONS-TASK-84_WORKFLOW-Deploy-Jekyll-site_JOB-build &amp;lt;--&#10;--&amp;gt; local GITEA-ACTIONS-TASK-84_WORKFLOW-Deploy-Jekyll-site_JOB-build-env &amp;lt;--&#10;local act-toolcache&#10;local blog_staging&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If I do not extract the build artifacts from the action run, they will be thrown away together with the action run when it has completed and everything is gone. The runner&amp;rsquo;s volume set up &lt;a href="https://blog.schallbert.de/en/gitea-action-runner-jekyll-dockerimage/#jekyll-build"&gt;above&lt;/a&gt; doesn&amp;rsquo;t touch the action as it again executes in an isolated image.&lt;/p&gt;&#10;&lt;p&gt;To really understand this has cost me a lot of time and many failing action runs. Don&amp;rsquo;t repeat my mistakes and read the according &lt;a href="https://docs.gitea.com/usage/actions/act-runner?_highlight=runner#register-the-runner-with-docker" target="_blank" rel="noopener noreferrer" class="external-link"&gt;section in Gitea&amp;rsquo;s documentation&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; thoroughly.&lt;/p&gt;&#10;&lt;p&gt;Well, this turns out more complicated than I thought. As a single maintainer of my website, this is like taking a sledgehammer to crack a nut. Anyways, I&amp;rsquo;ll follow through.&lt;/p&gt;&#10;&lt;h3 id="but-i-can-use-upload-artifact-right"&gt;But I can use &lt;code&gt;upload-artifact&lt;/code&gt;, right?&lt;/h3&gt;&#10;&lt;p&gt;Right? Well, at least I thought so. I modified my Action script accordingly and used the upload function from the Github Actions marketplace:&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;# workflows/jekyll-build-action.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:#75715e"&gt;# Automatically upload the build folder to Giteas blog repo folder&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;UPLOAD ARTIFACT ---&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;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/upload-artifact@v3&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;with&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;path&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;/workspace/schallbert/blog/&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;Blog_Staging&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;retention-days&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;Great, now I have a downloadable Zip file on Gitea&amp;rsquo;s web surface:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-12-09_uploadsuccessful.jpg" alt="Image: Gitea snapshot showing a successful artifact upload"&gt;&lt;/figure&gt;&#10;&lt;p&gt;But how do I now get that downloaded to the server&amp;rsquo;s disk? The action is fully encapsuled and cannot even access its underlying Docker Daemon (&lt;a href="https://blog.schallbert.de/en/gitea-action-runner-native-jekyll/#run-the-runner-on-the-hosts-operating-system"&gt;for good reasons, I guess&lt;/a&gt;).&lt;/p&gt;&#10;&lt;p&gt;So I search for the assets in the Gitea-Container that persists the artifacts at&lt;code&gt;/data/gitea/actions_artifacts/BUILD_ID&lt;/code&gt;. Unfortunately, they are a heap (hundreds) of &lt;code&gt;.chunk.gz&lt;/code&gt; files with cryptic numbers as name where I don&amp;rsquo;t know how to merge into one single archive.&lt;/p&gt;&#10;&lt;p&gt;And there&amp;rsquo;s another thing I don&amp;rsquo;t like about &lt;code&gt;upload-artifact&lt;/code&gt;: My website is becoming bigger and bigger due to an increasing amount of media that I use. That&amp;rsquo;s why the uploader requires &lt;em&gt;nearly two minutes&lt;/em&gt; for compressing and packing - with a linear ascending outlook.&lt;/p&gt;&#10;&lt;p&gt;Dead end.&lt;/p&gt;&#10;&lt;h3 id="also-docker-cp-wont-work-here"&gt;Also Docker &lt;code&gt;cp&lt;/code&gt; won&amp;rsquo;t work here&lt;/h3&gt;&#10;&lt;p&gt;Then I try using Docker&amp;rsquo;s copy operation to get the artifacts to my host machine:&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;# workflows/jekyll-build-action.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;name&lt;/span&gt;: --- &lt;span style="color:#ae81ff"&gt;COPY ARTIFACT ---&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;run&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;docker cp gitea-runner-1:/workspace/schallbert/blog /tmp/blog_staging&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This fails like so:&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;gitea-runner-1 | docker cp gitea-runner-1:/workspace/schallbert/blog /tmp/blog_staging&#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;gitea-runner-1 | &lt;span style="color:#f92672"&gt;[&lt;/span&gt;Deploy Jekyll site/build&lt;span style="color:#f92672"&gt;]&lt;/span&gt; | /var/run/act/workflow/3.sh: line 2: docker: not found&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 | &lt;span style="color:#f92672"&gt;[&lt;/span&gt;Deploy Jekyll site/build&lt;span style="color:#f92672"&gt;]&lt;/span&gt; ❌ Failure - Main --- COPY ARTIFACT ---&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This was expected as the action container knows no Docker. I cannot move files from &amp;ldquo;inside&amp;rdquo; the container to another one.&#10;Or can I? By forwarding the host machine&amp;rsquo;s Daemon &lt;code&gt;docker.sock&lt;/code&gt;, according to Gitea documentation, I might be tempted&amp;hellip; No, I will not continue here - see above, I&amp;rsquo;d puncture encapsulation if I did.&lt;/p&gt;&#10;&lt;h3 id="transfer-artifacts-via-sftp"&gt;Transfer artifacts via SFTP&lt;/h3&gt;&#10;&lt;p&gt;I&amp;rsquo;m running low on options. I think I could copy the artifacts via &lt;a href="https://de.wikipedia.org/wiki/SSH_File_Transfer_Protocol" target="_blank" rel="noopener noreferrer" class="external-link"&gt;SFTP&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; from within the &lt;code&gt;act_runner&lt;/code&gt;&amp;rsquo;s action image into the Webserver-Container&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;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-12-09_sftp_infrastructure.jpg" alt="Image: Machine architecture if I used SFTP for artifact share between Gitea and webserver"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Luckily, there already is an Action for this: &lt;a href="https://github.com/marketplace/actions/scp-files" target="_blank" rel="noopener noreferrer" class="external-link"&gt;scp-files&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Now I have to modify the webserver&amp;rsquo;s container in a way to be accessible via SCP, inhale an SSH-key, and save incoming artifacts.&lt;/p&gt;&#10;&lt;p&gt;On the other hand, &lt;a href="https://stackoverflow.com/questions/65381311/run-sshd-in-docker-container" target="_blank" rel="noopener noreferrer" class="external-link"&gt;people in multiple forums&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; say that this is not a clean solution. &amp;ldquo;Ac container should only accommodate one application. But there&amp;rsquo;s a solution again: &lt;a href="https://hub.docker.com/r/panubo/sshd/#!" target="_blank" rel="noopener noreferrer" class="external-link"&gt;A docker container with only the SSH-Daemon installed&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. If I have a shared volume between that one and my server, I might be successful.&lt;/p&gt;&#10;&lt;p&gt;Looks like more work still. Another container, piping files out and back into my system - that just doesn&amp;rsquo;t seem right. So I continue my research.&lt;/p&gt;&#10;&lt;h3 id="action-volumes"&gt;Handover a volume to the Action-Container&lt;/h3&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-12-09_gitea_infrastructure.jpg" alt="Image: Docker-Gitea infrastructure overview"&gt;&lt;/figure&gt;&#10;&lt;p&gt;All these failures and dead ends have been with me for over a month now. I still really want to be able to build, test and publish automatically. I&amp;rsquo;ve learned a lot along the way and now I&amp;rsquo;m hoping that this approach will finally get me to the solution.&lt;/p&gt;&#10;&lt;p&gt;During &lt;a href="https://gitea.com/gitea/act_runner/issues/329" target="_blank" rel="noopener noreferrer" class="external-link"&gt;my search&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; I came across &lt;a href="https://gitea.com/gitea/act_runner/src/branch/main/internal/pkg/config/config.example.yaml" target="_blank" rel="noopener noreferrer" class="external-link"&gt;the option to change the configuration of the runner&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. That way, I might end up including a volume that I can share between the action and my server via the daemon on the host.&lt;/p&gt;&#10;&lt;p&gt;If that works, I wouldn&amp;rsquo;t have an additional security risk like a publicly available &lt;code&gt;file transfer container&lt;/code&gt; or the long waiting times caused by &lt;code&gt;upload-artifact&lt;/code&gt;. So let&amp;rsquo;s get to work! There are &lt;a href="https://gitea.com/gitea/act_runner/issues/407" target="_blank" rel="noopener noreferrer" class="external-link"&gt;more reasons&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; why volumes can make sense in the respective action - so there must already be people out there who have managed this.&lt;/p&gt;&#10;&lt;p&gt;So I add a volume to the Action script:&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;# workflows/jekyll-build-action.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;container&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;jvconseil/jekyll-docker: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;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;/tmp/blog_staging:/blog_staging&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;/opt/cache:/opt/hostedtoolcache&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But the artifacts still don&amp;rsquo;t show up on my host&amp;rsquo;s filesystem. I strip down the action as much as I can to just check if the volume creates a folder in the action container:&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;# workflows/jekyll-build-action.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;steps&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;CHECK_VOLUME ---&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;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;&#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; ls -al /blog_staging&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But still:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;gitea-runner-1 | [Deploy Jekyll site/build] [DEBUG] Working directory &amp;#39;/workspace/schallbert/blog&amp;#39;&#10;gitea-runner-1 | [Deploy Jekyll site/build] | ls: /blog_staging: No such file or directory&#10;gitea-runner-1 | [Deploy Jekyll site/build] ❌ Failure - Main --- CHECK_VOLUME ---&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Some more, fruitless tries (maybe I had misspelled anything?) and extensive research in multiple forums I found out that there is a &lt;a href="https://gitea.com/gitea/act_runner/issues/329" target="_blank" rel="noopener noreferrer" class="external-link"&gt;valid_volumes&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; attribute in &lt;code&gt;act&lt;/code&gt;. If I don&amp;rsquo;t have the volume added here, it won&amp;rsquo;t bind to the container.&lt;/p&gt;&#10;&lt;p&gt;OK, let&amp;rsquo;s add it to the action script:&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;# jekyll-build-action.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;container&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;jvconseil/jekyll-docker: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;valid_volumes&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;#39;**&amp;#39;&lt;/span&gt; &lt;span style="color:#75715e"&gt;# This does not work. Also specialized lists indicating the volumes directly won&amp;#39;t work&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;/tmp/blog_staging:/blog_staging&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;/opt/cache:/opt/hostedtoolcache&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Still nothing. At least, now, I get a warning in the logs:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;gitea-runner-1 | [Deploy Jekyll site/build] [/tmp/blog_staging] is not a valid volume, will be ignored&#10;gitea-runner-1 | [Deploy Jekyll site/build] [/opt/cache] is not a valid volume, will be ignored&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Something is wrong with the transfer of this option. There are &lt;a href="https://gitea.com/gitea/act_runner/issues/407" target="_blank" rel="noopener noreferrer" class="external-link"&gt;reports&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; in which the registration of the volumes works. So I take the &lt;a href="https://gitea.com/gitea/act_runner/src/branch/main#configuration" target="_blank" rel="noopener noreferrer" class="external-link"&gt;instructions&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and create a configuration file where I enter the following:&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;# runner/config.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:#75715e"&gt;# Volumes (including bind mounts) can be mounted to containers. Glob syntax is supported, see https://github.com/gobwas/glob&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;# You can specify multiple volumes. If the sequence is empty, no volumes can be mounted.&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;# For example, if you only allow containers to mount the `data` volume and all the json files in `/src`, you should change the config to:&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;# valid_volumes:&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;# - data&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;# - /src/*.json&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;# If you want to allow any volume, please use the following configuration:&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;# valid_volumes:&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;#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:#f92672"&gt;valid_volumes&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;/tmp/blog_staging&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;/opt/hostedtoolcache&amp;#34;&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;Important:&lt;/em&gt; The notation must be a string, comma-separated, and always has to specify the &lt;code&gt;Source&lt;/code&gt; of the volume. In other words, the part that comes before the &lt;code&gt;:&lt;/code&gt;.&#10;However, the error message &amp;ldquo;not a valid volume&amp;rdquo; still appears when building.&lt;/p&gt;&#10;&lt;p&gt;But then I realize that the config must be made available to the &lt;code&gt;act_runner&lt;/code&gt; itself as a volume - otherwise the runner running in the container cannot access it at all!&lt;/p&gt;&#10;&lt;p&gt;So my &lt;code&gt;docker-compose.yml&lt;/code&gt; for Gitea, section &amp;ldquo;runner&amp;rdquo; now looks like this:&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: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;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;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;/opt/hostedtoolcache:/opt/hostedtoolcache&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;And, finally:&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;gitea-runner-1 | ls -al /blog_staging&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 | &lt;span style="color:#f92672"&gt;[&lt;/span&gt;Deploy Jekyll site/build&lt;span style="color:#f92672"&gt;]&lt;/span&gt; | total &lt;span style="color:#ae81ff"&gt;8&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 | &lt;span style="color:#f92672"&gt;[&lt;/span&gt;Deploy Jekyll site/build&lt;span style="color:#f92672"&gt;]&lt;/span&gt; | drwxr-xr-x &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; root root &lt;span style="color:#ae81ff"&gt;4096&lt;/span&gt; Dec &lt;span style="color:#ae81ff"&gt;27&lt;/span&gt; 07:32 .&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 | &lt;span style="color:#f92672"&gt;[&lt;/span&gt;Deploy Jekyll site/build&lt;span style="color:#f92672"&gt;]&lt;/span&gt; | drwxr-xr-x &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; root root &lt;span style="color:#ae81ff"&gt;4096&lt;/span&gt; Dec &lt;span style="color:#ae81ff"&gt;27&lt;/span&gt; 07:46 ..&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 | &lt;span style="color:#f92672"&gt;[&lt;/span&gt;Deploy Jekyll site/build&lt;span style="color:#f92672"&gt;]&lt;/span&gt; ✅ Success - Main --- CHECK_VOLUME ---&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;What an act. I&amp;rsquo;m so glad that everything is running smoothly now and that I can actually find the build files on my local host system! 🥳&lt;/p&gt;&#10;&lt;h2 id="reverse-proxy-connection-refused"&gt;One last tip&lt;/h2&gt;&#10;&lt;p&gt;If you work with a reverse proxy like I do and get strange &lt;code&gt;connection refused&lt;/code&gt; error messages when starting up the runner like:&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;docker compose up&#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; Running 2/0&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ✔ Container gitea Created 0.0s &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ✔ Container gitea-runner-1 Created 0.0s &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Attaching to gitea, gitea-runner-1&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 | level&lt;span style="color:#f92672"&gt;=&lt;/span&gt;info msg&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;Starting runner daemon&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 | level&lt;span style="color:#f92672"&gt;=&lt;/span&gt;error msg&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;fail to invoke Declare&amp;#34;&lt;/span&gt; error&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;unavailable: dial tcp &amp;lt;address&amp;gt;: connect: connection refused&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 | Error: unavailable: dial tcp &amp;lt;address&amp;gt; connect: connection refused&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea | Server listening on :: port 22.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner-1 exited with code &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then your reverse proxy is either not correctly configured or - like in my case - shut down.&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;Using SFTP (Secure File Transfer Protocol) which is file sharing per ssh (secure shell), I&amp;rsquo;d increase attack surface of my system just a bit. This is because the SSH client would be reachable for anyone and not just for my action.&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>🗙 Gitea Actions - Jekyll Workflow</title><link>https://blog.schallbert.de/en/gitea-action-runner-native-jekyll/</link><pubDate>Thu, 30 Nov 2023</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/gitea-action-runner-native-jekyll/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-11-30_ghaction-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: github actions workflow to build Jekyll page"&#10; title="🗙 Gitea Actions - Jekyll Workflow" /&gt;&#10;&lt;p&gt;After setting up my own little server in &lt;a href="https://blog.schallbert.de/en/projects/move-blog-to-own-server/"&gt;this project&lt;/a&gt; and adding all necessary applications to serve my blog, I now want to automatically build, integrate, and deploy.&lt;/p&gt;&#10;&lt;h2 id="desired-outcome"&gt;Desired outcome&lt;/h2&gt;&#10;&lt;p&gt;This is how I want it to be: When my Gitea instance detects a &lt;code&gt;git push&lt;/code&gt; to the remote origin of my blog&amp;rsquo;s repository, I want an action runner to start a Docker container. That container shall load Ruby, Bundler, Jekyll, and all other required dependencies. Then it shall checkout the blog&amp;rsquo;s source files and then run the &lt;code&gt;bundle exec jekyll build&lt;/code&gt; command. When its output is ready in the &lt;code&gt;_site&lt;/code&gt; folder, I want the runner to self-terminate and to free all occupied resources.&lt;/p&gt;&#10;&lt;p&gt;At this point I&amp;rsquo;d like to manually check my blog&amp;rsquo;s integrity, maybe also add some automated checks like detecting broken links or similar.&lt;/p&gt;&#10;&lt;p&gt;Finally, I want to be able to have the site deployed and thus made available to the public with another simple command.&lt;/p&gt;&#10;&lt;h2 id="starting-point-github-actions"&gt;Starting point: Github Actions&lt;/h2&gt;&#10;&lt;p&gt;As usual, I first search other people&amp;rsquo;s solutions for the same problem. I found the &lt;a href="https://github.com/actions/starter-workflows/blob/main/pages/jekyll.yml" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Github Actions starter-workflows&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Repository, created by Github themselves, and copied most of it into my action as a first shot:&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;# jekyll-build-pages.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;# Sample workflow for building and deploying a Jekyll site&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;Deploy Jekyll site&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;run-name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;${{ gitea.actor }} builds Jekyll site&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;on&lt;/span&gt;: [&lt;span style="color:#ae81ff"&gt;push]&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;jobs&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;# Build job&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;build&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;runs-on&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ubuntu-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;steps&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;Checkout&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;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/checkout@v3&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;Setup Ruby&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;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ruby/setup-ruby@55283cc23133118229fd3f97f9336ee23a179fcf&lt;/span&gt; &lt;span style="color:#75715e"&gt;# v1.146.0&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;with&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;ruby-version&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;3.1&amp;#39;&lt;/span&gt; &lt;span style="color:#75715e"&gt;# Not needed with a .ruby-version file&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;bundler-cache&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt; &lt;span style="color:#75715e"&gt;# runs &amp;#39;bundle install&amp;#39; and caches installed gems automatically&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;cache-version&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#75715e"&gt;# Increment this number if you need to re-download cached gems&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After uploading this workflow to my blog at &lt;code&gt;workflows/jekyll.yml&lt;/code&gt;, the runner indeed started its work:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-11-30_rubyfails.jpg" alt="Image: Gitea action runner returns error on ruby install"&gt;&lt;/figure&gt;&#10;&lt;h2 id="problems-installing-ruby"&gt;Problems installing Ruby&lt;/h2&gt;&#10;&lt;p&gt;That was a quick win. On the other hand, the runner wasn&amp;rsquo;t even able to successfully install Ruby. Here&amp;rsquo;s the corresponding log:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;::error::The current runner (debian-11-x64) was detected as self-hosted because the platform does not match a GitHub-hosted runner image &#10;(or that image is deprecated and no longer supported).&#10;In such a case, you should install Ruby in the $RUNNER_TOOL_CACHE yourself, for example using https://github.com/rbenv/ruby-build&#10;You can take inspiration from this workflow for more details: &#10;https://github.com/ruby/ruby-builder/blob/master/.github/workflows/build.yml&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The error message looks reasonable and is well-placed.&lt;/p&gt;&#10;&lt;p&gt;Background: My runner operates in Docker which uses a stripped-down Debian image named &lt;code&gt;node:16-bullseye&lt;/code&gt; &lt;a href="https://github.com/nektos/act/blob/master/IMAGES.md" target="_blank" rel="noopener noreferrer" class="external-link"&gt;(Referenz)&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Looks like it is incompatible with the Github Actions Runner which executes atop Ubuntu22.&lt;/p&gt;&#10;&lt;p&gt;There are quite some options to resolve:&lt;/p&gt;&#10;&lt;h3 id="troubleshooting-options"&gt;Troubleshooting options&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Run the runner on Ubuntu22, use &lt;a href="https://docs.gitea.com/1.23/usage/actions/act-runner/#labels" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;code&gt;labels&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; for that.&lt;/li&gt;&#10;&lt;li&gt;Don&amp;rsquo;t use Docker but have the runner operate on the host&amp;rsquo;s operating system&lt;/li&gt;&#10;&lt;li&gt;Use prebuilt Ruby, installed in the runner&amp;rsquo;s cache&lt;/li&gt;&#10;&lt;li&gt;Use an out-of-the-box &lt;a href="https://blog.schallbert.de/en/gitea-action-runner-jekyll-dockerimage/"&gt;Jekyll-Dockerimage&lt;/a&gt; to not require additional installation steps&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="operate-the-runner-with-ubuntu22-instead-of-node16"&gt;Operate the runner with Ubuntu22 instead of Node16&lt;/h3&gt;&#10;&lt;p&gt;There is a matching &lt;a href="https://hub.docker.com/_/ubuntu" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Dockerimage&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, but it will take a lot more disk space and RAM than a minimalistic one. Plus, it takes a little more time until it is live. Let&amp;rsquo;s see whether there is a more simple solution.&lt;/p&gt;&#10;&lt;h3 id="run-the-runner-on-the-hosts-operating-system"&gt;Run the runner on the host&amp;rsquo;s operating system&lt;/h3&gt;&#10;&lt;p&gt;This has considerable security drawbacks as &lt;a href="https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches" target="_blank" rel="noopener noreferrer" class="external-link"&gt;unprotected branches&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; might allow third-party injected runners via &lt;code&gt;git push --force&lt;/code&gt;. In an extreme case, it would blindly execute malware right on my server system. In addition, I&amp;rsquo;d kill portability by hardwiring the runner to a machine&amp;rsquo;s OS.&lt;/p&gt;&#10;&lt;h3 id="use-pre-built-ruby-in-runner-toolcache"&gt;Use pre-built Ruby in runner toolcache&lt;/h3&gt;&#10;&lt;p&gt;As proposed by the error message, I followed &lt;a href="https://about.gitea.com/resources/tutorials/enable-gitea-actions-cache-to-accelerate-cicd" target="_blank" rel="noopener noreferrer" class="external-link"&gt;another manual&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; to map a volume containing prebuilt ruby on my host using Gitea&amp;rsquo;s &lt;code&gt;docker-compose.yml&lt;/code&gt;. In addition, I assigned the targeted folder to the &lt;code&gt;RUNNER_TOOL_CACHE&lt;/code&gt; environment variable:&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;GITEA_INSTANCE_URL=&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:#ae81ff"&gt;GITEA_RUNNER_NAME=&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:#ae81ff"&gt;RUNNER_TOOL_CACHE=/opt/hostedtoolcache&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;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/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;/opt/hostedtoolcache:/opt/hostedtoolcache&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;Here, I map the host&amp;rsquo;s filesystem path &lt;code&gt;/opt/hostedtoolcache&lt;/code&gt; to Docker&amp;rsquo;s &lt;code&gt;/opt/hostedtoolcache&lt;/code&gt;. Docker creates the folders automatically &amp;ldquo;on both sides&amp;rdquo; if they are not yet existing on next &lt;code&gt;docker compose up&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Let&amp;rsquo;s try to install Ruby on the host machine.&lt;/p&gt;&#10;&lt;p&gt;So I download the &lt;a href="https://github.com/rbenv/ruby-build" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;code&gt;ruby-build&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; repository via &lt;code&gt;git clone&lt;/code&gt; and have the installer run &lt;code&gt;./ruby-build/install.sh&lt;/code&gt;.&#10;Then, I execute the build with the requested target path from the error message:&lt;code&gt;ruby-build 3.1.4 /opt/hostedtoolcache/Ruby/3.1.4/x64&lt;/code&gt;&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;==&amp;gt; Downloading openssl-3.1.4.tar.gz...&#10; % Total % Received % Xferd Average Speed Time Time Time Current&#10; Dload Upload Total Spent Left Speed&#10;100 14.8M 100 14.8M 0 0 81.3M 0 --:--:-- --:--:-- --:--:-- 81.1M&#10;==&amp;gt; Installing openssl-3.1.4...&#10;&#10;BUILD FAILED (Ubuntu 22.04 on x86_64 using ruby-build 20231114)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="problems-with-prebuilt-ruby"&gt;Problems with prebuilt-Ruby&lt;/h3&gt;&#10;&lt;p&gt;The logs shows the problem:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;[...] No C compiler found, please specify one with the environment variable CC [...]&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So I type &lt;code&gt;apt install build-essential&lt;/code&gt; and confirm with &lt;code&gt;which gcc&lt;/code&gt; that I now have a C compiler installed at &lt;code&gt;/usr/bin/gcc&lt;/code&gt; Let&amp;rsquo;s try again!&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;crypto/comp/c_zlib.c:36:11: fatal error: zlib.h: No such file or directory&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hmm, let&amp;rsquo;s load it via &lt;code&gt;apt install libz-dev&lt;/code&gt; and re-run the insall. This time it takes a while, then:&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;*** Following extensions are not compiled:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;openssl:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Could not be configured. It will not be installed.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; /tmp/ruby-build.20231205114312.9174.0gmhNf/ruby-3.1.4/ext/openssl/extconf.rb:100: OpenSSL library could not be found. You might&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Check ext/openssl/mkmf.log &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; more details.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;readline:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Could not be configured. It will not be installed.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; /tmp/ruby-build.20231205114312.9174.0gmhNf/ruby-3.1.4/ext/readline/extconf.rb:62: Neither readline nor libedit was found&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Check ext/readline/mkmf.log &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; more details.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;*** Fix the problems, &lt;span style="color:#66d9ef"&gt;then&lt;/span&gt; remove these directories and try again &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; you want.&#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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;OK, openssl this time. Get it with &lt;code&gt;apt install libssl-dev&lt;/code&gt; and &lt;code&gt;apt install libreadline-dev&lt;/code&gt; and finally :&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;Installed ruby-3.1.4 to /opt/hostedtoolcache/Ruby/3.1.4/x64&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;!🎉!&lt;/p&gt;&#10;&lt;h2 id="still-no-success-with-the-action"&gt;Still no success with the action&lt;/h2&gt;&#10;&lt;p&gt;Strange, it is the same error all over again:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;💬 ::debug::isExplicit: 3.1.4&#10;&#10;gitea-runner-1 | [Deploy Jekyll site/build] 💬 ::debug::checking cache: /opt/hostedtoolcache/Ruby/3.1.4/x64&#10;gitea-runner-1 | [Deploy Jekyll site/build] | ::debug::checking cache: /opt/hostedtoolcache/Ruby/3.1.4/x64&#10;gitea-runner-1 | [Deploy Jekyll site/build] 💬 ::debug::not found&#10;gitea-runner-1 | [Deploy Jekyll site/build] | ::debug::not found&#10;gitea-runner-1 | [Deploy Jekyll site/build] ❗ ::error::The current runner (debian-11-x64) was detected as self-hosted&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This results from the following &lt;a href="https://github.com/ruby/setup-ruby/blob/master/ruby-builder.js#L97" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Action&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; code:&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-js" data-lang="js"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// setup-ruby/ruby-builder.js&#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;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;common&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;shouldUseToolCache&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;engine&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;version&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;inToolCache&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;common&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;toolCacheFind&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;engine&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;version&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:#a6e22e"&gt;inToolCache&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;rubyPrefix&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;inToolCache&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; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;toolCacheRubyPrefix&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;common&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;getToolCacheRubyPrefix&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;platform&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;engine&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;version&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:#a6e22e"&gt;common&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;isSelfHostedRunner&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;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;rubyBuildDefinition&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;engine&lt;/span&gt; &lt;span style="color:#f92672"&gt;===&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;ruby&amp;#39;&lt;/span&gt; &lt;span style="color:#f92672"&gt;?&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;version&lt;/span&gt; &lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;engine&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;-&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;version&lt;/span&gt;&lt;span style="color:#e6db74"&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;core&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;error&lt;/span&gt;( [...] )&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Causes for missing `toolcache`&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2024-01-06T00:00:00Z"&gt;&#10; 2024-01-06&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; There are two other possible reasons why the &lt;code&gt;toolcache&lt;/code&gt; cannot be found: The volume is made available to the runner but is not passed on to the action (see this &lt;a href="https://blog.schallbert.de/en/build-deploy-hugo-with-actions-docker-caddy/#docker-shared-volumes/"&gt;post on shared volumes&lt;/a&gt;). As a result, the data is not available at the actual destination. Or the &lt;code&gt;toolcache&lt;/code&gt; path is not included in the runner’s configuration file under the &lt;code&gt;valid_volumes&lt;/code&gt; entry. According to a &lt;a href="https://gitea.com/gitea/act/pulls/60" target="_blank" rel="noopener noreferrer" class="external-link"&gt;pull request thread&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; it is auto-added, though. Still, I&amp;rsquo;m not sure if that information is outdated as I&amp;rsquo;m migrating at a phase where the runner is in active development.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;h3 id="check-docker-volumes"&gt;Check docker volumes&lt;/h3&gt;&#10;&lt;p&gt;Hmpf. Let&amp;rsquo;s see whether Ruby is really available in that volume. To do this, I use &lt;code&gt;docker ps&lt;/code&gt; to get the runner&amp;rsquo;s container ID and enter it here:&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;docker exec -it &amp;lt;containerID&amp;gt; bash&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then I navigate to &lt;code&gt;cd /opt/hostedtoolcache&lt;/code&gt;, list its contents with &lt;code&gt;ls&lt;/code&gt; and there it is: &lt;code&gt;Ruby&lt;/code&gt;. It is there but still cannot be found in the Action.&lt;/p&gt;&#10;&lt;h2 id="accept-my-failure"&gt;Accept my failure&lt;/h2&gt;&#10;&lt;p&gt;No way to proceed for me at this point 😖. I opened this problem to the community as &lt;a href="https://gitea.com/gitea/act_runner/issues/441" target="_blank" rel="noopener noreferrer" class="external-link"&gt;act_runner cannot find hostedtoolcache&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and will try&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://blog.schallbert.de/en/gitea-action-runner-jekyll-dockerimage/"&gt;Another option&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="root-cause"&gt;Root cause&lt;/h3&gt;&#10;&lt;p&gt;Looks like the support of &lt;code&gt;setup-ruby&lt;/code&gt; action for self-hosted runners like mine might have been &lt;a href="https://github.com/ruby/setup-ruby/issues/475#issuecomment-1455099634" target="_blank" rel="noopener noreferrer" class="external-link"&gt;discontinued&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Maybe the documentation, creating the above error message, has not been updated accordingly.&lt;/p&gt;&#10;</description></item><item><title>Multi-language site - l10n</title><link>https://blog.schallbert.de/en/jekyll-polyglot-language-support/</link><pubDate>Tue, 14 Nov 2023</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/jekyll-polyglot-language-support/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-11-14_translations_polyglot_flags-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Flags that can be clicked on to change display language"&#10; title="Multi-language site - l10n" /&gt;&#10;&lt;p&gt;Providing a website in different languages is called &amp;ldquo;internationalization&amp;rdquo; - in short &lt;a href="https://en.wikipedia.org/wiki/Internationalization_and_localization" target="_blank" rel="noopener noreferrer" class="external-link"&gt;i16n&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; or &amp;ldquo;localization&amp;rdquo; - &lt;code&gt;l10n&lt;/code&gt;. If you search these abbreviations in the web, you&amp;rsquo;ll find a lot of useful material. By the way, the numbers in the brief version stand for the letter count in the word&amp;rsquo;s center.&lt;/p&gt;&#10;&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;&#10;&lt;p&gt;Right from the start, I was asked to &lt;a href="https://hobbyline.info/forum/index.php?thread/603-blog-artikel-zur-portalfr%C3%A4se/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;prepare a German version&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; of this website. To date I considered this &lt;a href="https://hobbyline.info/forum/index.php?thread/603-blog-artikel-zur-portalfr%C3%A4se/&amp;amp;postID=7619#post7619" target="_blank" rel="noopener noreferrer" class="external-link"&gt;too much effort&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and this only changed half a year ago when I was verbally asked by multiple people independently from each other.&lt;/p&gt;&#10;&lt;p&gt;So I invested half a year of evenings to get the heavy lifting done, transcribing roughly 80 articles. But I still wasn&amp;rsquo;t quite done.&lt;/p&gt;&#10;&lt;h2 id="jekyll-extension"&gt;Jekyll extension&lt;/h2&gt;&#10;&lt;p&gt;I looked at a couple of extensions for Jekyll dedicated to multilingual support like &lt;a href="https://demondehellis.github.io/en/posts/how-to-make-jekyll-blog-multilingual/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;demondehellis&amp;rsquo;s plugin-free solution&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://github.com/untra/polyglot" target="_blank" rel="noopener noreferrer" class="external-link"&gt;untra&amp;rsquo;s polyglot plugin&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://github.com/kurtsson/jekyll-multiple-languages-plugin" target="_blank" rel="noopener noreferrer" class="external-link"&gt;kurtsson&amp;rsquo;s jekyll-multiple-languages-plugin&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and some other solutions to publish a website in multiple languages.&lt;/p&gt;&#10;&lt;p&gt;So I had to decide for one. The plugin-less solution was out quickly because I wanted a concise naming convention where identical posts in different languages have identical names. Plus, I wanted interlinks to be persistent for the selected language. Something would have to keep language pots separated.&lt;/p&gt;&#10;&lt;p&gt;After some research, I found &lt;a href="https://leo3418.github.io/collections/multilingual-jekyll-site/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;leo3418&amp;rsquo;s&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; wonderful series of blog posts for implementing Polyglot - and so I decided to go with that one.&lt;/p&gt;&#10;&lt;h3 id="polyglot-installation"&gt;Polyglot: Installation&lt;/h3&gt;&#10;&lt;p&gt;Installation of PolyGlot is simple because both the documentation in the &lt;a href="https://github.com/untra/polyglot" target="_blank" rel="noopener noreferrer" class="external-link"&gt;repository&amp;rsquo;s&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;code&gt;readme.md&lt;/code&gt; and leo3418&amp;rsquo;s related &lt;a href="https://leo3418.github.io/collections/multilingual-jekyll-site/set-up-polyglot.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;post&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; do instruct these first steps in detail. They do this so well so I won&amp;rsquo;t lose another word about it here.&lt;/p&gt;&#10;&lt;h3 id="restructuring-the-website"&gt;Restructuring the website&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-11-14_folderstructure.jpg" alt="image: updated folder structure for this site"&gt;&lt;/figure&gt;&#10;Polyglot offers two possibilities to adapt the site&amp;rsquo;s structure for multilanguage support:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Use the language abbreviation in the post&amp;rsquo;s filename like this: &lt;code&gt;2023-11-14-jekyll-polyglot-language-support-en.md&lt;/code&gt;, repeat for each article in a different language with the appropriate abbreviation&lt;/li&gt;&#10;&lt;li&gt;Create folders named like the language code, e.g. &lt;code&gt;/en&lt;/code&gt; and put all english content in there, but keep file names the same between languages.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;I decided for the latter option out of laziness - I didn&amp;rsquo;t want to rename existing articles. Polyglot manages to stick to the same language when navigating a site. If you, for instance, click &lt;a href="https://blog.schallbert.de/en/milling-small-parts/"&gt;https://schallbert.de/milling-small-parts/&lt;/a&gt;, Polyglot will assume you want the English version to be presented and thus provide it. Where this doesn&amp;rsquo;t work anymore is section titles as they are translated themselves, invalidating attached links, so the section link &lt;a href="https://blog.schallbert.de/en/jekyll-polyglot-language-support/#restructuring-the-website"&gt;#restructuring-the-website&lt;/a&gt; will lead to a 404 fault when called in the German version.&lt;/p&gt;&#10;&lt;p&gt;Thus, when using section titles, the full post&amp;rsquo;s path has to be noted.&lt;/p&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Permalink-IDs&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2026-05-11T00:00:00Z"&gt;&#10; 2026-05-11&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; Alternatively, you can assign a language-independent ID to the heading so that the anchor links pointing to the headings remain identical. Example: &lt;code&gt;## Restructuring the website {#website-restructuring}&lt;/code&gt;, accessed via the link &lt;code&gt;#website-restructuring&lt;/code&gt;.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;h3 id="translating-page-title-navigation-header-footer"&gt;Translating page title, navigation, header, footer&lt;/h3&gt;&#10;&lt;p&gt;This is getting a little more complicated. Jekyll&amp;rsquo;s &lt;code&gt;_config.yml&lt;/code&gt; is intended for one version of the website only, so we cannot just create two variants of this file and expect it to work. To still enable full translation, we&amp;rsquo;ll have to overwrite language sensitive parameters like &lt;code&gt;site.title&lt;/code&gt; or &lt;code&gt;site.description&lt;/code&gt; externally depending on selected locale.&lt;/p&gt;&#10;&lt;p&gt;I have solved this adding &lt;code&gt;l10n.yml&lt;/code&gt; files in the &lt;code&gt;_data&lt;/code&gt; folder that covers these parameters for each language, and then packing them into a language-code folder like we&amp;rsquo;re used to do with our posts. To show their content, the relevant liquid parameters in &lt;code&gt;html&lt;/code&gt; files (&lt;code&gt;_includes&lt;/code&gt; folder) have to be modified to now search there instead of taking the standard config. Example: &lt;code&gt;{{ site.data.ui-text[site.data.l10n.locale].follow_label | remove: &amp;quot;:&amp;quot; | default: &amp;quot;Follow&amp;quot; }}&lt;/code&gt;. German version of this site will show &lt;strong&gt;FOLGEN:&lt;/strong&gt; while the English one says &lt;strong&gt;FOLLOW:&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;h3 id="language-selector"&gt;Language selector&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-11-14_translations_polyglot_flags.jpg" alt="image: footer section of this website"&gt;&lt;/figure&gt;&#10;Of course I want to offer the option to select a locale on every page. To do this, I added the language code and its flag to the footer. On click, it will reload the page in the requested locale. The following code I wrote in Liquid enables this behavior:&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:#f92672"&gt;%&lt;/span&gt; raw &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:#f92672"&gt;%&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; lang in site.languages &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:#f92672"&gt;%&lt;/span&gt; assign url_parts &lt;span style="color:#f92672"&gt;=&lt;/span&gt; page.url &lt;span style="color:#f92672"&gt;|&lt;/span&gt; split: &lt;span style="color:#e6db74"&gt;&amp;#34;/&amp;#34;&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:#f92672"&gt;%&lt;/span&gt; assign lang_code &lt;span style="color:#f92672"&gt;=&lt;/span&gt; lang &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;&#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; assign collection_name &lt;span style="color:#f92672"&gt;=&lt;/span&gt; url_parts[&lt;span style="color:#ae81ff"&gt;1&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:#f92672"&gt;%&lt;/span&gt; assign post_url &lt;span style="color:#f92672"&gt;=&lt;/span&gt; url_parts &lt;span style="color:#f92672"&gt;|&lt;/span&gt; last &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;&#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:#66d9ef"&gt;if&lt;/span&gt; lang &lt;span style="color:#f92672"&gt;==&lt;/span&gt; site.default_lang &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:#f92672"&gt;%&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; collection_name &lt;span style="color:#f92672"&gt;==&lt;/span&gt; post_url &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:#f92672"&gt;%&lt;/span&gt; assign lang_url &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;/&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; append: post_url &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:#f92672"&gt;%&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;else&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:#f92672"&gt;%&lt;/span&gt; assign lang_url &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;/&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; append: collection_name &lt;span style="color:#f92672"&gt;|&lt;/span&gt; append: &lt;span style="color:#e6db74"&gt;&amp;#34;/&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; append: post_url &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:#f92672"&gt;%&lt;/span&gt; endif &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:#f92672"&gt;%&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; url_parts.size &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&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:#f92672"&gt;%&lt;/span&gt; assign lang_url &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;/&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; append: lang_code &lt;span style="color:#f92672"&gt;|&lt;/span&gt; append: &lt;span style="color:#e6db74"&gt;&amp;#34;/&amp;#34;&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:#f92672"&gt;%&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; collection_name &lt;span style="color:#f92672"&gt;==&lt;/span&gt; post_url &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:#f92672"&gt;%&lt;/span&gt; assign lang_url &lt;span style="color:#f92672"&gt;=&lt;/span&gt; lang_url &lt;span style="color:#f92672"&gt;|&lt;/span&gt; append: post_url &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:#f92672"&gt;%&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;else&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:#f92672"&gt;%&lt;/span&gt; assign lang_url &lt;span style="color:#f92672"&gt;=&lt;/span&gt; lang_url &lt;span style="color:#f92672"&gt;|&lt;/span&gt; append: collection_name &lt;span style="color:#f92672"&gt;|&lt;/span&gt; append: lang_url &lt;span style="color:#f92672"&gt;|&lt;/span&gt; append: post_url &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:#f92672"&gt;%&lt;/span&gt; endif &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:#f92672"&gt;%&lt;/span&gt; endif &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:#f92672"&gt;&amp;lt;&lt;/span&gt;a href&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34; {{ lang_url }}&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;{{ site.data[lang].l10n.lang_name }}&lt;span style="color:#f92672"&gt;&amp;lt;/&lt;/span&gt;a&lt;span style="color:#f92672"&gt;&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:#f92672"&gt;%&lt;/span&gt; endfor &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:#f92672"&gt;%&lt;/span&gt; endraw &lt;span style="color:#f92672"&gt;%&lt;/span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here I&amp;rsquo;m splitting the current page &lt;code&gt;url&lt;/code&gt;. The last section will be the &lt;code&gt;post_url&lt;/code&gt;. The algo assumes that the first part is the post&amp;rsquo;s collection name, e.g. &lt;code&gt;posts-hardware&lt;/code&gt;. The following logic inserts the language code into the url if and where needed so that the correct full url is built depending on the used collection and page navigation depth.&lt;/p&gt;&#10;&lt;h2 id="publishing"&gt;Publishing&lt;/h2&gt;&#10;&lt;p&gt;I was pretty proud of this elegant way of selecting languages. I thing it all fits in nicely and the multi-language support potentially adds a good number of readers. The big downer is the duplicated work for creating each post in two languages.&lt;/p&gt;&#10;&lt;p&gt;Unfortunately, the site now wasn&amp;rsquo;t compatible anymore with Github Pages - the auto release would show each page twice instead of keeping them separated by language. Two options remained: &lt;a href="https://blog.schallbert.de/en/struggling-github-actions/"&gt;creating custom Github Actions&lt;/a&gt; to publish on Github Pages, or to &lt;a href="https://blog.schallbert.de/en/projects/move-blog-to-own-server/"&gt;move the whole site to an own server&lt;/a&gt;. I&amp;rsquo;ll decide for the latter to be on the safe side regarding data protection regulations because in that case I can make sure the hoster is situated in Germany.&lt;/p&gt;&#10;&lt;p&gt;Well, this upcoming project is too big to take on in this post.&lt;/p&gt;&#10;</description></item><item><title>Milling small parts</title><link>https://blog.schallbert.de/en/milling-small-parts/</link><pubDate>Wed, 01 Nov 2023</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/milling-small-parts/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-11-01_smallparts-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Milled small part, size comparison to a coin"&#10; title="Milling small parts" /&gt;&#10;&lt;h2 id="why-milling"&gt;Why milling?&lt;/h2&gt;&#10;&lt;p&gt;It is sometimes hard to manufacture small parts in low quantities, especially when their dimensional tolerances have to be low. In this case, die casting wouldn&amp;rsquo;t be economical. Hobbyist 3D-printing machines wouldn&amp;rsquo;t deliver the low tolerances I need and affordable lasers wouldn&amp;rsquo;t be able to cut parts that are 2.5D (i.e. that don&amp;rsquo;t have an even surface). Plus, I&amp;rsquo;m not a fan of ordering parts from sweat shops half a globe away.&lt;/p&gt;&#10;&lt;p&gt;But still, milling these parts is perfectly possible. This video shows how:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;iframe&#10; src="https://makertube.net/videos/embed/64TS5d5cp2eNqf8SSPvY77"&#10; title="Milling Small Parts"&#10; loading="lazy"&#10; allow="autoplay; fullscreen; picture-in-picture"&#10; allowfullscreen&#10; style="width: 100%; height: auto; min-height: 315px; aspect-ratio: 16/9;"&#10; &gt;&lt;/iframe&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Milling Small Parts&lt;/span&gt;&lt;a href="https://makertube.net/videos/embed/64TS5d5cp2eNqf8SSPvY77" class="attr-link" target="_blank" rel="noopener noreferrer"&gt;↗&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h3 id="what-accessories-are-required"&gt;What accessories are required?&lt;/h3&gt;&#10;&lt;p&gt;All you need is:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;A vacuum table&lt;/li&gt;&#10;&lt;li&gt;Vacuum pump, preferably supporting deep vacuum levels&lt;/li&gt;&#10;&lt;li&gt;Some plastic wrap&lt;/li&gt;&#10;&lt;li&gt;A Vacuum fleece&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="what-is-that-part-for"&gt;What is that part for?&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-11-01_smallparts_soapbutler.jpg" alt="Image: Video splash preview with soapbutler"&gt;&lt;/figure&gt;&#10;&lt;p&gt;The part I&amp;rsquo;m fabricating in the video belongs to one of my &lt;a href="https://blog.schallbert.de/en/projects/seifenbutler/"&gt;soap butlers&lt;/a&gt;. The current version can be mounted to any shower rail with diameter &lt;code&gt;18-25mm&lt;/code&gt;, and needs this small part for clamping securely to the rail. It is in contact with the rail from the rear and both locks the soap butler in place, limiting its possible &amp;ldquo;wobble&amp;rdquo; movement in Z-direction.&lt;/p&gt;&#10;&lt;p&gt;This way, not only soap bar shampoos can be placed here, but also much heavier shampoo bottles.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-11-01_soapbutler_shampoobottle.jpg" alt="Image: black soapbutler holding a shampoo bottle"&gt;&lt;/figure&gt;&#10;&lt;p&gt;The part has to fit in snugly as it shall be able to slide without noticeable play, but still has to move effortlessly to adjust to different shower rail diameters.&lt;/p&gt;&#10;&lt;h2 id="what-else-do-i-need-to-consider"&gt;What else do I need to consider?&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;plastic wrap: make sure the cutouts you do are not too big. Else, the plastic wrap will be pulled in by the endmill, wrap it, and leave marks on the workpiece. It imbalances the cutter and may cause additional damage this way. To avoid this, use double-sided tape to secure the wrap where you do the cutouts.&lt;/li&gt;&#10;&lt;li&gt;vacuum fleece: Note that the fleece will be compressed when you apply vacuum. Z-zero height of workpiece surface has to be taken with vacuum applied to get accurate results.&lt;/li&gt;&#10;&lt;li&gt;Don&amp;rsquo;t cut all the way through. Leave an onion skin. Its strength depends on the material you work with. For PMMA I use &lt;code&gt;0.3mm&lt;/code&gt;, for wood &lt;code&gt;0.7mm&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;I don&amp;rsquo;t account for wrap strength. I know that thicker protective wraps e.g. for PMMA and Dibond have a strength of &lt;code&gt;20-30µm&lt;/code&gt; (where I calculate with it), so I&amp;rsquo;d assume the plastic wrap I use is less than &lt;code&gt;10µm&lt;/code&gt; which is in the magnitude of machine accuracy.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;</description></item><item><title>DIY edge finder</title><link>https://blog.schallbert.de/en/touching-off/</link><pubDate>Sun, 21 May 2023</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/touching-off/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-05-21_edgefinder-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: DIY low-cost edge finder"&#10; title="DIY edge finder" /&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-05-21_edge_taperedtool.jpg" alt="Image: getting XY0 of a workpiece visually"&gt;&lt;/figure&gt;&#10;As a CNC operator, I sometimes do have to know where my workpiece ends. Therefore I need its edges&amp;rsquo; &lt;code&gt;X/Y&lt;/code&gt; coordinates. I was kind of solving this problem to date by using a tapered tool which I would manually position above the workpiece corner the best I could, visually controlling its position with help of my phone&amp;rsquo;s camera zoom.&lt;/p&gt;&#10;&lt;h2 id="the-problem"&gt;The problem&lt;/h2&gt;&#10;&lt;p&gt;This is relatively quick to setup and accuracy is high enough for most jobs. But it depends on the line of sight where even slight deviations create large errors so you have to be pretty thorough. When I started doing &lt;a href="https://blog.schallbert.de/en/cut-dibond/"&gt;double-sided workpieces like this&lt;/a&gt;, I felt the accuracy just is not high enough anymore.&lt;/p&gt;&#10;&lt;h2 id="solution"&gt;Solution&lt;/h2&gt;&#10;&lt;p&gt;I was discussing my problem with the machine manufacturer, thinking they might hint me towards buying an edge detector &amp;ldquo;3D finder&amp;rdquo; or similar product. But wrong I was!&#10;Instead, I was able to build an edge finder that did not cost me anything at all and in no time.&lt;/p&gt;&#10;&lt;h3 id="method"&gt;Method&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-05-21_build.jpg" alt="Image: Things needed to construct the edge finder"&gt;&lt;/figure&gt;&#10;Roy was like &amp;ldquo;Well, really the only thing you need is a ball bearing. Put it onto a pole and mount it like any other tool. Have the spindle turn at low RPM. Then slowly have the machine move towards the workpiece edge. Once the outer ring of the bearing stops, you&amp;rsquo;ll hear and see it, and stop the machine movement. That&amp;rsquo;s the position you are searching for. Offset it with the bearing&amp;rsquo;s radius and you&amp;rsquo;re done.&amp;rdquo;&lt;/p&gt;&#10;&lt;h3 id="the-build"&gt;The build&lt;/h3&gt;&#10;&lt;p&gt;I was intrigued by that idea. As I don&amp;rsquo;t have access to a good hardware store that sells bearings and stuff nearby, I was searching the household. And voilà, my kids own a fidget spinner they didn&amp;rsquo;t use for a while. So I&amp;hellip; snitched it. It contains a 22mm bearing with 8mm bore. Ok next - what should I use as a shaft?&lt;/p&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Also well-suited: Ball bearings from inline skates, skateboards etc.&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2023-06-04T00:00:00Z"&gt;&#10; 2023-06-04&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; Right after publishing this blog entry, a reader informed me that inline skates are also a perfect source for ball bearings in the household.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-05-21_edgefinder.jpg" alt="Image: The mounted edge finder"&gt;&lt;/figure&gt;&#10;I went for a dull drill bit, cut it in half, shoved the bearing on and added a drop of glue to secure it in place - Done!&lt;/p&gt;&#10;&lt;h2 id="execution"&gt;Execution&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Mount the edgefinder and jog the machine close to the workpiece you&amp;rsquo;d like to measure.&lt;/li&gt;&#10;&lt;li&gt;Have the spindle turn at low RPM, e.g. &lt;code&gt;S500&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Optional: To keep things simple, switch to machine coordinates&lt;/li&gt;&#10;&lt;li&gt;Make the machine move towards the workpiece, e.g. with command &lt;code&gt;G01 X5.0 F1&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Prepare to hit &amp;ldquo;routine stop&amp;rdquo; once the bearing stops turning&lt;/li&gt;&#10;&lt;li&gt;Note down machine coordinates of where you stopped or set &lt;code&gt;G92&lt;/code&gt; offset to here&lt;/li&gt;&#10;&lt;li&gt;Repeat with other axes if required&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2023-05-22_edgefinder_action.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Simple edgefinder in action.&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;</description></item><item><title>Hardware Test</title><link>https://blog.schallbert.de/en/testing-hardware/</link><pubDate>Sun, 23 Apr 2023</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/testing-hardware/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-04-23_subsystem_test-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Subsystem boundary diagram"&#10; title="Hardware Test" /&gt;&#10;&lt;h2 id="a-new-drive"&gt;A new drive&lt;/h2&gt;&#10;&lt;p&gt;Recently, I bought a &lt;a href="https://blog.schallbert.de/en/projects/spindle-upgrade/"&gt;spindle upgrade&lt;/a&gt;. I now switched to a 3-phase induction motor with forced air cooling that requires a Variable Frequency Drive so I can set different speeds. The system takes the same analogue 0-10V signal as my previous motor. For the numerical controller it looks the same with one exception: The VFD can now create an emergency stop signal if something goes wrong.&lt;/p&gt;&#10;&lt;p&gt;As you can imagine, the new spindle subsystem is by far more complex than the old one. I had to buy, wire, and setup a dedicated electric control box.&lt;/p&gt;&#10;&lt;h2 id="so-why-testing"&gt;So why testing?&lt;/h2&gt;&#10;&lt;p&gt;With higher complexity comes higher risk of failure. There are more electrical and mechanical parts involved that each can behave incorrectly or even be destroyed if connected improperly. As I am just a human being who makes mistakes, I&amp;rsquo;d better not jump in at the deep end. So, while wiring everything up, I wondered how such a system should be systematically tested.&lt;/p&gt;&#10;&lt;h2 id="test-strategy"&gt;Test Strategy&lt;/h2&gt;&#10;&lt;p&gt;I borrowed the different test scopes from my job in the software industry. I think they fit in quite well:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Unit&lt;/strong&gt;: Test that verifies behavior of a component of the system in isolation. All external influences are removed or mocked away. Example: Check if the blower turns if supplied with its nominal voltage.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Integration&lt;/strong&gt;: These tests integrate different components that provide a common functionality. Example: Verify that spindle cooling works with delayed shutdown.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Subsystem&lt;/strong&gt;: Test that verifies the correct interaction of all components within a subsystem. Example: The spindle speed changes when feeding the analogue input of the VFD with different voltages.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;System (End2End)&lt;/strong&gt;: Tests that verify a system from the end-user perspective. Example: When pressing the emergency off switch of the CNC, does the VFD bring the spindle to a halt safely?&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-04-23_test_scopes.jpg" alt="Image: different test scopes of my CNC system"&gt;&lt;/figure&gt;&#10;&lt;h3 id="verify-early"&gt;Verify early&lt;/h3&gt;&#10;&lt;p&gt;From my experience, it makes sense to perform tests at the earliest possible point in time. After the spindle was delivered, I was able to check right away whether all bearings were smooth-running, if the fan would turn when voltage was applied, and see whether the motor windings were within expected resistance range, and so on. I could do these unit-level tests even before I started building or wiring the control box.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2023-04-23-blowertest.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Unit test: Spindle cooling fan&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;This might save a lot of troubleshooting when something doesn&amp;rsquo;t seem to work correctly during comissioning. And if you find something this early, you can directly get in touch with the manufacturer of the affected component to request an exchange while not being blocked on the other end.&lt;/p&gt;&#10;&lt;h3 id="bottom-up-testing"&gt;Bottom-up testing&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-04-23_test_pyramid.jpg" alt="Image: my test pyramid"&gt;&lt;/figure&gt;&#10;The four levels of testing are my verification strategy here. I try to have many simple and quick early test on unit level, and just a few complex system tests that each involve a multitude of preparation and test steps, being executed just once upon initial startup when the build is complete.&lt;/p&gt;&#10;&lt;p&gt;When you look at the concept of the &lt;a href="https://martinfowler.com/testing/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;test pyramid&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; that is used a lot in software testing, it may be applicable for hardware tests as well to some extent, maybe with a different number of layers and varying names for the levels of integration.&lt;/p&gt;&#10;&lt;h2 id="test-plan"&gt;Test plan&lt;/h2&gt;&#10;&lt;p&gt;I don&amp;rsquo;t usually write test plans for hobby projects because the systems I design are rarely as complex as this. Plus, most of them do not require dangerous voltages to operate unlike this one.&lt;/p&gt;&#10;&lt;p&gt;To better visualize the system, I drew a layout of all components with the most important inputs and outputs.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-04-23_system_layout.jpg" alt="Image: motor spindle subsystem layout"&gt;&lt;/figure&gt;&#10;&lt;p&gt;From that image, I derived a test plan. I tried to cover each component&amp;rsquo;s interactions with each other, ending up in more unit than system test cases.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-04-23_test_plan.jpg" alt="Image: A test plan for the motor spindle subsystem"&gt;&lt;/figure&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2023-04-23-powersupplytest.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Unit test: Supply &amp;#43;24V rail OK&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;Here I checked whether the cooling fan power supply operates correctly.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2023-04-23-relaytest.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Unit test: Time delay setting OK&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;With this test I verify that the cooling fan relay configuration and activation work as expected. For the purpose of this test, I selected shorter delay times than in the later application.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2023-04-23-vfdtest.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Integration test: VFD initial operation&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;The variable frequency drive is able to turn the spindle in the correct direction of rotation at different speeds.&lt;/p&gt;&#10;&lt;h2 id="test-execution"&gt;Test execution&lt;/h2&gt;&#10;&lt;p&gt;Of course, there were many more hardware-based tests that I didn&amp;rsquo;t mention in the above plan. Things like is the spindle correctly aligned to the Z-axis? Have I fastened all clamps with the appopriate torque? Are all connectors inserted and locked?&lt;/p&gt;&#10;&lt;p&gt;Before trying to run through material on the CNC I performed all the tests and actually found one issue on the system level: I had a pinning error in the spindle speed analogue output of the numerical controller&amp;rsquo;s signal harness so that the spindle motor system wouldn&amp;rsquo;t start despite the run signal being present.&lt;/p&gt;&#10;&lt;p&gt;This just took minutes to figure out because I had subsystem-tested the VFD before (Test case: &amp;ldquo;Reacts to speed command&amp;rdquo;), knowing that it cannot be the culprit.&lt;/p&gt;&#10;&lt;h2 id="documents"&gt;Documents&lt;/h2&gt;&#10;&lt;p&gt;In case you plan on adding a VFD-controlled spindle motor to your machine, please find my full test plan below.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://blog.schallbert.de/assets/docs/SpindleTests.pdf"&gt;VFD-controlled spindle motor test plan&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;But please take this plan with a grain of salt and note the usual disclaimer:&lt;/p&gt;&#10;&lt;p&gt;⚠️ &lt;strong&gt;Risk of electric shock.&lt;/strong&gt; ⚠️&lt;/p&gt;&#10;&lt;p&gt;This kind of system should only be built or worked on by professionals. Especially avoid touching live parts or VFD components before the VFD&amp;rsquo;s bleeder resistors have had enough time to discharge the intermediate circuit&amp;rsquo;s bulk capacitors.&lt;/p&gt;&#10;</description></item><item><title>Dibond Tests</title><link>https://blog.schallbert.de/en/cut-dibond-tests/</link><pubDate>Fri, 10 Mar 2023</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/cut-dibond-tests/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-03-10_dibond_test-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Dibond engrave test runs"&#10; title="Dibond Tests" /&gt;&#10;&lt;p&gt;Note: The engravings I disuss in this article are relatively fine. Font size is between 3mm and 6mm, and workpiece size is 80x80mm. Viewed at a distance of an ell or more, the quality issues are not apparent and the overall impression is quite good. Still, I wanted to improve.&lt;/p&gt;&#10;&lt;h2 id="further-quality-tuning-on-dibond"&gt;Further quality tuning on Dibond&lt;/h2&gt;&#10;&lt;p&gt;After I got mediocre results with my &lt;a href="https://blog.schallbert.de/en/cut-dibond/"&gt;previous milling project&lt;/a&gt;, I decided to do some more test runs in Dibond. Before I started those, I noted down possible causes for rough cutting edges that I saw earlier.&lt;/p&gt;&#10;&lt;p&gt;Then, I&amp;rsquo;ll try to rule out noise factors to track down root causes, and in a further step I will do my best to eliminate most of the issues.&lt;/p&gt;&#10;&lt;h3 id="possible-causes-for-poor-quality"&gt;Possible causes for poor quality&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/cut-dibond-tests/#cutter-inspection"&gt;Worn-out or dull cutter&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/cut-dibond-tests/#cutter-geometry"&gt;Cutter geometry&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/cut-dibond-tests/#speed-feed"&gt;Improper Feed / Speed parameters&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/cut-dibond-tests/#milling-style"&gt;Climb milling vs. conventional milling&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/cut-dibond-tests/#engrave-depth"&gt;Too shallow engrave depth&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/cut-dibond-tests/#machine-vibrations"&gt;Machine vibrations&lt;/a&gt; on portal acceleration and/or deceleration, especially on Y-axis&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/cut-dibond-tests/#spindle-mount"&gt;Router motor mount not stiff enough&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/cut-dibond-tests/#spindle-imbalance"&gt;Imbalanced cutter / collet&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="cutter-inspection"&gt;Cutter inspection&lt;/h3&gt;&#10;&lt;p&gt;I&amp;rsquo;m not an expert on cutters, but I cannot imagine that a carbide bit is worn out after half an hour of cutting through &lt;code&gt;0.3mm&lt;/code&gt;thick &lt;code&gt;AlMg1&lt;/code&gt; which is the harder compound of Dibond composite material. Also, the cutting edge seems to be in a good shape. The only thing I noticed was that there seems to be a thin layer of Aluminium sticking to the tip of the bit. Maybe this affects cut quality?&lt;/p&gt;&#10;&lt;p&gt;Carefully, I removed the aluminium residues with a trimming knife and minimal sanding. As good as new!&lt;/p&gt;&#10;&lt;aside class="update-box update-box--warn" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ⚠️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Use cutter with one material only&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2023-10-30T00:00:00Z"&gt;&#10; 2023-10-30&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; I used the same engraving chisels - which have been in use for about 4 hours by now - to work with acrylic (PMMA). The cut is rougher than with new engraving cutters. Acrylic requires very sharp tools. I therefore recommend always using one tool for each material.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;h3 id="cutter-geometry"&gt;Cutter geometry&lt;/h3&gt;&#10;&lt;p&gt;The cutters I&amp;rsquo;m using for this test do not have chip flutes to guide shavings away from the place of cut. There are others on the market which do provide one and I wonder if it makes a difference. Maybe a spiral flute could improve quality of cut?&lt;/p&gt;&#10;&lt;p&gt;Background behind that thesis is that for cutting letters, my CAM won&amp;rsquo;t use the defined dive ramp but plunges straight into the material. When you have a closer look at the images below, you might even see marks in the clearcoat around the place where the cutter hit Z0, shearing the surface away but not yet expediting the chip away from the cut.&lt;/p&gt;&#10;&lt;p&gt;On the other side, a spiral flute will try to pull the aluminium layer upwards. As it is &amp;ldquo;only&amp;rdquo; thermally bonded to the polyethylene core, adhesive force might be too low, especially with fine engravings or letters like &amp;ldquo;e, a, o, R, B&amp;hellip;&amp;rdquo;&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;Summary&lt;/em&gt;: Buy spiral flute engrave cutters, try them, and document the results 😅&lt;/p&gt;&#10;&lt;h3 id="speed-feed"&gt;Improper Speed / Feed parameters&lt;/h3&gt;&#10;&lt;p&gt;I double-checked required spindle RPM for my engrave cutter which is single-flute. I again got a value higher than my maximum speed of &lt;code&gt;24000RPM&lt;/code&gt;, so I can confirm to use my motor&amp;rsquo;s maximum speed with that bit.&lt;/p&gt;&#10;&lt;p&gt;To try out other feed rates, I created a quick 2D design in my CAD and had my CAM run it in different feeds: &lt;code&gt;F600mm/min&lt;/code&gt;, &lt;code&gt;F800mm/min&lt;/code&gt;, and &lt;code&gt;F1000mm/min&lt;/code&gt;.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-03-10_workpiece.jpg" alt="Image: My Test workpiece with tryout engravings"&gt;&lt;/figure&gt;&#10;&lt;p&gt;There are a lot of things that we can observe here:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Look at the labels for the different feed rates. The font is so small, that the engrave cutter won&amp;rsquo;t even fully cut through the upper Aluminium layer. It rather &amp;ldquo;mashes&amp;rdquo; the aluminium into the polyethylene layer below, and squeezes the aluminium sideways.&lt;/li&gt;&#10;&lt;li&gt;Although I change the feed rates by as much as 20% per spiral turn, quality of cut remains the same. This confirms that for my &lt;code&gt;6mm 0.5tip 90°&lt;/code&gt; cutter in Dibond, a feed rate of &lt;code&gt;F1000mm/min&lt;/code&gt; is fine.&lt;/li&gt;&#10;&lt;li&gt;It is clear to see that the &amp;ldquo;outer&amp;rdquo; edges of the engrave operation are rough. I can feel this when I stroke the workpiece with my finger.&lt;/li&gt;&#10;&lt;li&gt;The &amp;ldquo;inner&amp;rdquo; edges of the engravings are really smooth, though. They gleam in contrary to their matte counterparts.&lt;/li&gt;&#10;&lt;li&gt;The black, inner parts of the spiral are shaped very even and without cutter marks or a lot of remaining chips.&lt;/li&gt;&#10;&lt;li&gt;The capital &amp;ldquo;T&amp;rdquo; of the word test runs looks a bit strange as there are vertical marks. Let&amp;rsquo;s come to this later.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;&lt;em&gt;Summary&lt;/em&gt;: Speed and feed parameters are fine and don&amp;rsquo;t seem to be the culprit for poor machining quality in this case.&lt;/p&gt;&#10;&lt;h3 id="milling-style"&gt;Climb milling vs. conventional milling&lt;/h3&gt;&#10;&lt;p&gt;To get a clear view on the effects of &lt;a href="https://blog.schallbert.de/en/portal-milling/#milling-style"&gt;milling strategy&lt;/a&gt;, let&amp;rsquo;s take a detailed look at the spiral.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-03-10_spiral.jpg" alt="Image: Detail view of the spiral, cut with the engrave bit"&gt;&lt;/figure&gt;&#10;&lt;p&gt;It is easy to see that the outer cut has very clean edges while the inner one has not. The spiral has been milled outwards to inwards, and the spindle turns clockwise. The groove was cut in a single pass for the narrower line, and with a smoothing increment of &lt;code&gt;Z-0.1mm&lt;/code&gt; for the wider one.&lt;/p&gt;&#10;&lt;p&gt;This means that conventional milling has been applied to the outer edge of the groove, and the inner edge was machined with climb milling, i.e. the cutter was moving in the direction of cut and not against it.&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;Summary&lt;/em&gt;: Although climb milling is preferred on a CNC for various reasons, this is not true for aluminium composite materials like Dibond. Here, conventional milling is the strategy to choose if you want clean chamfers.&lt;/p&gt;&#10;&lt;h3 id="engrave-depth"&gt;Too shallow engrave depth&lt;/h3&gt;&#10;&lt;p&gt;&lt;em&gt;Summary&lt;/em&gt;: As the images above show, it is a problem if the aluminium layer is not fully broken through. In some follow-up experiments I tested a minimum viable engrave depth and ended up with a value of ~150% of the aluminium layer&amp;rsquo;s strength, in my case that is &lt;code&gt;0.45mm&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Note that this constraint might only be valid for V-cutters that don&amp;rsquo;t feature spiral cuts or cutter flutes.&lt;/p&gt;&#10;&lt;h3 id="too-high-engrave-depth"&gt;Too high engrave depth&lt;/h3&gt;&#10;&lt;p&gt;This was a guess that came up when looking at the images from my &lt;a href="https://blog.schallbert.de/en/cut-dibond/#quality-analysis"&gt;previous experiment with Dibond&lt;/a&gt;. To compare, I created two spiral engrave paths with different depths of &lt;code&gt;Z-0.5mm&lt;/code&gt; and &lt;code&gt;Z-1.0mm&lt;/code&gt;. While the former wouldn&amp;rsquo;t make any difference when I add another pass for smoothing, it makes the latter engrave look a little more clean.&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;Summary&lt;/em&gt;: For deeper engraves, smoothing runs are helpful but not a must. Instead, a much bigger lever to improve quality is to use conventional milling only.&lt;/p&gt;&#10;&lt;h3 id="machine-vibrations"&gt;Machine vibrations&lt;/h3&gt;&#10;&lt;p&gt;Remember the test texts carrying strange marks? At the beginning I thought this was due to the brushed-look surface: It is not actually brushed but imprinted under high pressure using rollers. But when I had a closer look, another assumption came to my mind: Machine vibrations.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-03-10_text.jpg" alt="Image: Test texts with different feed rates."&gt;&lt;/figure&gt;&#10;&lt;p&gt;The texts have been made with different feeds, where &lt;code&gt;6&lt;/code&gt; indicates &lt;code&gt;600mm/min&lt;/code&gt; and so on. Below on the right I have prepared a second image that has an extra zoom into the &amp;ldquo;t&amp;quot;s for better comparison of the feed rates. Actually, the quicker engrave settings look better than the slower ones.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-03-10_ts.jpg" alt="Image: Macro zoom into the `t`&amp;#39;s of the text."&gt;&lt;/figure&gt;&#10;I have been excercising the limits of my machine a lot in the past half a year. I came up with pretty high values on acceleration and maximum velocity which I verified the machine was capable to work with.&#10;&amp;ldquo;Unusually high&amp;rdquo; is actually what the manufacturer of my controller software said when I contacted their support with an unrelated issue about a warning message &lt;a href="https://blog.schallbert.de/en/projects/one-year-zerspanobert/#velocityerror"&gt;&amp;ldquo;Velocity was higher than max!&amp;rdquo;&lt;/a&gt; a couple of weeks ago.&lt;/p&gt;&#10;&lt;p&gt;That said, to verify my hypothesis I created yet another test sample in CAD and CAM where I&amp;rsquo;d only carve Test texts and straight lines with sharp corners so that the machine has to accelerate and decelerate to full carve speed and back to zero more often.&lt;/p&gt;&#10;&lt;p&gt;This time I&amp;rsquo;d keep the feedrate constant at &lt;code&gt;f1000mm/min&lt;/code&gt; but change the machine&amp;rsquo;s acceleration values to see whether the acceleration unsettles the machine&amp;rsquo;s portal system in a way that shakes are propagated to the tool tip.&lt;/p&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&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-0"&#10; aria-label="Image: Front view of used V-cut bit. "&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-10_vcut_front.jpg"&#10; alt="Image: Front view of used V-cut bit. "&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-1"&#10; aria-label="Image: 45° Front view of used V-cut bit. It carries some residual aluminium material."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-10_vcut_front45.jpg"&#10; alt="Image: 45° Front view of used V-cut bit. It carries some residual aluminium material."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-2"&#10; aria-label="Image: Side view of used V-cut bit. The cutting edge looks sharp and polished."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-10_vcut_side.jpg"&#10; alt="Image: Side view of used V-cut bit. The cutting edge looks sharp and polished."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-3"&#10; aria-label="Image: Rear view of used V-cut bit. At the very tip, it is more shiny than at the rest of its body."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-10_vcut_back.jpg"&#10; alt="Image: Rear view of used V-cut bit. At the very tip, it is more shiny than at the rest of its body."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-4"&#10; aria-label="Image: Test text and numbers. At 800mm/s² acceleration, there are visible horizontal marks in the path and the quality of cut is poor."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-10_test800.jpg"&#10; alt="Image: Test text and numbers. At 800mm/s² acceleration, there are visible horizontal marks in the path and the quality of cut is poor."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-5"&#10; aria-label="Image: Test text and numbers. At 400mm/s² the highly detailed text does not look too good. There are chatter marks everywhere."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-10_test400.jpg"&#10; alt="Image: Test text and numbers. At 400mm/s² the highly detailed text does not look too good. There are chatter marks everywhere."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-6"&#10; aria-label="Image: Test text and numbers. Even with lowest acceleration, the problem is to stay. The aluminium layer tends to squash into the much softer polyurethane core material, creating mediocre results."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-10_test200.jpg"&#10; alt="Image: Test text and numbers. Even with lowest acceleration, the problem is to stay. The aluminium layer tends to squash into the much softer polyurethane core material, creating mediocre results."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;/div&gt;&lt;div class="hugo-gallery__caption"&gt;&#10; Testergebnisse: Detailaufnahmen von Stichel und Material bei variabler Maschinenbeschleunigung.&#10; &lt;/div&gt;&lt;/div&gt;&#10;&#10; &lt;div class="hugo-gallery__full" aria-live="polite"&gt;&lt;figure&#10; id="gallery-full-0"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-10_vcut_front.jpg"&#10; alt="Image: Front view of used V-cut bit. "&gt;&lt;figcaption&gt;The 6mm 90° 0.5m tip V-cut tool. At the cutting edge, there&amp;#39;s a little built-up material.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-1"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-10_vcut_front45.jpg"&#10; alt="Image: 45° Front view of used V-cut bit. It carries some residual aluminium material."&gt;&lt;figcaption&gt;It has been used for roughly 30minutes. The cutting edge looks intact.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-2"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-10_vcut_side.jpg"&#10; alt="Image: Side view of used V-cut bit. The cutting edge looks sharp and polished."&gt;&lt;figcaption&gt;Minimal residues of Aluminium seem to stick to its tip.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-3"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-10_vcut_back.jpg"&#10; alt="Image: Rear view of used V-cut bit. At the very tip, it is more shiny than at the rest of its body."&gt;&lt;figcaption&gt;Also here, the tip colour is different than the rest of the bit.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-4"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-10_test800.jpg"&#10; alt="Image: Test text and numbers. At 800mm/s² acceleration, there are visible horizontal marks in the path and the quality of cut is poor."&gt;&lt;figcaption&gt;Result with high machine acceleration&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-5"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-10_test400.jpg"&#10; alt="Image: Test text and numbers. At 400mm/s² the highly detailed text does not look too good. There are chatter marks everywhere."&gt;&lt;figcaption&gt;Result with medium machine acceleration&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-6"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-10_test200.jpg"&#10; alt="Image: Test text and numbers. Even with lowest acceleration, the problem is to stay. The aluminium layer tends to squash into the much softer polyurethane core material, creating mediocre results."&gt;&lt;figcaption&gt;Result with low machine acceleration.&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&#10;&lt;/section&gt;&#10;&#10;&lt;p&gt;You can see that the vertical marks become less visible on lower acceleration settings. As I had to reset the machine after each parameter change, I must re-measure Z0 height everytime. On the lowest acceleration setting, I probably had a grain of dust below the sensor as the depth of cut is visibly less than on the other two test samples which deteriorates the engrave quality.&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;Question: &amp;ldquo;Did the machine reach target speed at all?&amp;rdquo;&lt;/em&gt; Text height is &lt;code&gt;s0=6mm&lt;/code&gt;, so for example at &lt;code&gt;a=800mm/s²&lt;/code&gt;, the machine takes &lt;code&gt;t=f/a=20ms&lt;/code&gt; to accelerate to its target feed rate. Travelling a distance of &lt;code&gt;s=a/2*t^2=0.17mm&lt;/code&gt; while accelerating, the machine will run &lt;code&gt;s1=6-2*0.17=5.66mm&lt;/code&gt; with target speed within the vertical stem of letter &lt;code&gt;t&lt;/code&gt;. This proves that the machine actually gets to run its targeted feedrate. Basic kinematic laws come in handy when working with CNC machines from time to time.&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;Summary&lt;/em&gt;: Make sure your machine runs in a fine-tuned parameter envelope relative to its size, stiffness, weight, and control variables so you avoid judder.&lt;/p&gt;&#10;&lt;h3 id="spindle-mount"&gt;Router motor mount&lt;/h3&gt;&#10;&lt;p&gt;I know this could be a problem from my past experiments where at some point &lt;a href="https://blog.schallbert.de/en/cnc-router-overload/"&gt;I overloaded my machine&lt;/a&gt;. But for engraving, the spindle runs at just 20% of maximum load.&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;Summary&lt;/em&gt;: This is not an issue here.&lt;/p&gt;&#10;&lt;h3 id="spindle-imbalance"&gt;Imbalanced cutter / collet&lt;/h3&gt;&#10;&lt;p&gt;&lt;em&gt;Summary&lt;/em&gt;: If this was the case, I would have either expected strange sounds or vibrations emanating from my machine. Maybe also edges with irregular shapes or torn chips could be a side effect. Neither of that is the case, so I&amp;rsquo;d exclude this noise factor here.&lt;/p&gt;&#10;&lt;h2 id="process-and-machine-updates"&gt;Process and machine updates&lt;/h2&gt;&#10;&lt;p&gt;Here&amp;rsquo;s what I actually did:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Inspect and clean cutters after usage&lt;/li&gt;&#10;&lt;li&gt;Try to avoid climb milling in dibond. Rather have the engrave path a little wider, and have both edges of the engrave cut conventionally.&lt;/li&gt;&#10;&lt;li&gt;Add a smoothing run when engrave depth exceeds &lt;code&gt;Z-0.6mm&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Reduce machine&amp;rsquo;s maximum acceleration values to minimize cutter marks.&lt;/li&gt;&#10;&lt;li&gt;Prepare a tryout CAD file to experiment with for optimal feeds and speeds when trying new materials.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-03-10_dibondmetallic.jpg" alt="Image: Dibond metallic, cut after all improvements were applied"&gt;&lt;/figure&gt;&#10;&lt;p&gt;See for yourself how the quality improved after applying all these changes.&lt;/p&gt;&#10;</description></item><item><title>Working with Aluminium composites</title><link>https://blog.schallbert.de/en/cut-dibond/</link><pubDate>Thu, 02 Mar 2023</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/cut-dibond/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-03-02_btc_beermat-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Engraving Dibond, required manual rework"&#10; title="Working with Aluminium composites" /&gt;&#10;&lt;h2 id="experiments-with-dibond"&gt;Experiments with Dibond&lt;/h2&gt;&#10;&lt;p&gt;A friend of mine is currently starting a business. He wanted something to give away to his customers, something that they would remember so they&amp;rsquo;d return to him. It should not be the standard pen or cup, but something that is somehow more close to what he is doing. Something metally. Something Like a business card, but with a three-dimensional feel. Something engraved.&lt;/p&gt;&#10;&lt;p&gt;While searching for material on the internet, I came across aluminium composites - a sandwich material made of two thin sheets of aluminium with a core of &lt;a href="https://en.wikipedia.org/wiki/Polyethylene" target="_blank" rel="noopener noreferrer" class="external-link"&gt;polyethylene&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, also known under its trade names Dibond®, Dilite®, Alcubond® etc. It is available coated, blank or even coloured and, optionally, in different finishes.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Its PE core is black, so it might be an ideal material for engraving&amp;rdquo;, I thought.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;So I ordered some samples and showed them to my friend. He was blown away, and that&amp;rsquo;s where this story starts.&lt;/p&gt;&#10;&lt;h3 id="the-coaster-concept"&gt;The Coaster Concept&lt;/h3&gt;&#10;&lt;p&gt;We brainstormed about what to do with that material. It should be simple to design and not too large so it could be an easy giveaway.&lt;/p&gt;&#10;&lt;p&gt;I know that aluminium composites can be machined in a way that they form 3-dimensional objects (&lt;a href="https://www.youtube.com/watch?v=9h0RVwTpabk" target="_blank" rel="noopener noreferrer" class="external-link"&gt;youtube video link&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;). But this seemed to complicated for a start. Our next idea was the one that I&amp;rsquo;m discussing here:&lt;/p&gt;&#10;&lt;p&gt;Engraving Coasters. Round or rectangles with smooth corners, even custom shapes are possible.&lt;/p&gt;&#10;&lt;p&gt;I started some tries with engraving cutters that looked promising - but not perfect. The aluminium layer tends to smudge and wants to evade the cutter so it presses into the soft polyethylene core when diving into the material at a 90° angle. Still, my friend wanted me to proceed so I bought a couple of sheets and continued experimenting.&lt;/p&gt;&#10;&lt;h3 id="cutters-speeds-and-feeds"&gt;Cutters, speeds and feeds&lt;/h3&gt;&#10;&lt;p&gt;As the coasters are below 100x100mm, I am using small endmills:&lt;/p&gt;&#10;&lt;p&gt;| Cutter (carbide) | Teeth | Dia | Speed | Feed XY | Feed Z | Z+ |&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;&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;[mm]&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;[RPM]&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;[mm/min]&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;[mm]&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;[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;acryl 30° upcut&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;2&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;1600&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;800&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;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;engrave 90°, 0.5mm tip&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;6&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;1000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;500&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1.5&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;The values may seem a bit aggressive, but I think I need to have a certain speed to not melt down the PE core. Chips do look good - short and thick - with these values and the machine gives a confidential hum when working, so I assume all is fine.&lt;/p&gt;&#10;&lt;h3 id="the-first-part"&gt;The first part&lt;/h3&gt;&#10;&lt;p&gt;I cut the coaster holder first. It consists of four parts that can simply be put together. They don&amp;rsquo;t need glue as clearances are tight. I use a fence as orientation for the XY-zero of the workpiece as I want to do two-sided milling. This enables me to do the cutouts as a last step, keeping vacuum pressure on my table until that very last step. Plus, the idea was that I wouldn&amp;rsquo;t have to manually rework the parts.&lt;/p&gt;&#10;&lt;p&gt;The first try was rubbish - front and rear milling paths weren&amp;rsquo;t perfectly aligned, and as little as some tenths of a millimeter make a great difference here, so I had to fix the root cause (edges on the fence I did not notice before that I had to hone down) and try again.&lt;/p&gt;&#10;&lt;p&gt;Here&amp;rsquo;s the result:&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-03-02_btc_holder.jpg" alt="Image: The coaster holder, made of Aluminium composite"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;m pretty happy how this turned out. I didn&amp;rsquo;t have to do any manual rework, it just fell out of the CNC like this. But I noticed that the chamfers I added to give the material a smooth touch were pretty different. While the ones on the &amp;ldquo;rear&amp;rdquo; that only cut &lt;code&gt;0.3mm&lt;/code&gt; off the edge where the upcut had previously done the part&amp;rsquo;s cutout were really good while the ones on the other side, where the tool had to cut &lt;code&gt;0.85mm&lt;/code&gt; deep into the material to add the chamfer, were a lot rougher (but still acceptable).&lt;/p&gt;&#10;&lt;h3 id="a-batch-of-six"&gt;A batch of six&lt;/h3&gt;&#10;&lt;p&gt;Then I selected a &amp;ldquo;simple-to-manufacture&amp;rdquo; logo - the Bitcoin Lightning symbol - and planned some basic engrave paths to have it carved. Here, I also did two-sided milling, which would not fully cut through the material but would leave an &amp;ldquo;onion skin&amp;rdquo; at the bottom which would be removed at a second pass on the bottom side while chamfering the edges.&lt;/p&gt;&#10;&lt;p&gt;I needed roughly 20 minutes to complete the jobs. Unfortunately, the fence I use to have a defined part edge that would stay in position even if turned around to mill the second side proved to be low repeatibility in terms of accuracy. The cutout on the bottom was off just one or two tenths of a millimeter, which in my case would double the total misalignment when turning it over its diagonal axis. That way, a part of my coaster&amp;rsquo;s outline had a very deep chamfer while the other half didn&amp;rsquo;t have a chamfer at all.&lt;/p&gt;&#10;&lt;p&gt;I need to find a fix for that. In the meantime, I manually sanded down the edges until I thought it&amp;rsquo;s enough so nobody would get hurt.&lt;/p&gt;&#10;&lt;p&gt;Then, I cleared remaining chips along the engrave paths with a toothbrush, and took the title photo above.&lt;/p&gt;&#10;&lt;h2 id="quality-analysis"&gt;Quality analysis&lt;/h2&gt;&#10;&lt;p&gt;Upon inspecting the part in more detail, I made the following observations:&lt;/p&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&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-0"&#10; aria-label="Image: BTC coasters on the machine, already fully cut."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-02_btc_cutouts.jpg"&#10; alt="Image: BTC coasters on the machine, already fully cut."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-1"&#10; aria-label="Image: A coaster holder: A U-shaped assembly holds up to six round coasters."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-02_holder_cutouts.jpg"&#10; alt="Image: A coaster holder: A U-shaped assembly holds up to six round coasters."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-2"&#10; aria-label="Image: Closeup of coaster&amp;#39;s bottom area. The edges look rough on deeper engravings. There is no uniform, shiny cut."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-02_engrave_closeup.jpg"&#10; alt="Image: Closeup of coaster&amp;#39;s bottom area. The edges look rough on deeper engravings. There is no uniform, shiny cut."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-3"&#10; aria-label="Image: Closeup of coaster&amp;#39;s lightning area. Its cutting edges look clean and shiny, the engraving is pretty shallow."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-02_engrave_closeup2.jpg"&#10; alt="Image: Closeup of coaster&amp;#39;s lightning area. Its cutting edges look clean and shiny, the engraving is pretty shallow."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-4"&#10; aria-label="Image: Closeup of coaster&amp;#39;s top area. Due to the slow feed, PU chips have molten to the cutting channel."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-02_engrave_closeup3.jpg"&#10; alt="Image: Closeup of coaster&amp;#39;s top area. Due to the slow feed, PU chips have molten to the cutting channel."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-5"&#10; aria-label="Image: Closeup of coaster&amp;#39;s lightning area, focusing on the deeper engrave. There are some marks on the aluminium edge."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-02_engrave_closeup4.jpg"&#10; alt="Image: Closeup of coaster&amp;#39;s lightning area, focusing on the deeper engrave. There are some marks on the aluminium edge."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;/div&gt;&lt;div class="hugo-gallery__caption"&gt;&#10; Prototyping coasters made of aluminium sandwich material.&#10; &lt;/div&gt;&lt;/div&gt;&#10;&#10; &lt;div class="hugo-gallery__full" aria-live="polite"&gt;&lt;figure&#10; id="gallery-full-0"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-02_btc_cutouts.jpg"&#10; alt="Image: BTC coasters on the machine, already fully cut."&gt;&lt;figcaption&gt;Top sheet machining complete. Cutout and engravings.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-1"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-02_holder_cutouts.jpg"&#10; alt="Image: A coaster holder: A U-shaped assembly holds up to six round coasters."&gt;&lt;figcaption&gt;Coaster holder cutouts, bottom view&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-2"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-02_engrave_closeup.jpg"&#10; alt="Image: Closeup of coaster&amp;#39;s bottom area. The edges look rough on deeper engravings. There is no uniform, shiny cut."&gt;&lt;figcaption&gt;Rough edges where engravings are more deep&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-3"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-02_engrave_closeup2.jpg"&#10; alt="Image: Closeup of coaster&amp;#39;s lightning area. Its cutting edges look clean and shiny, the engraving is pretty shallow."&gt;&lt;figcaption&gt;Less wide engravings looks more clean.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-4"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-02_engrave_closeup3.jpg"&#10; alt="Image: Closeup of coaster&amp;#39;s top area. Due to the slow feed, PU chips have molten to the cutting channel."&gt;&lt;figcaption&gt;Residues and chips left in the engrave channels&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-5"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/posts/2023-03-02_engrave_closeup4.jpg"&#10; alt="Image: Closeup of coaster&amp;#39;s lightning area, focusing on the deeper engrave. There are some marks on the aluminium edge."&gt;&lt;figcaption&gt;Wondering why engrave quality reduces on deeper engravings&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&#10;&lt;/section&gt;&#10;&#10;&lt;h3 id="all-parts-look-the-same"&gt;All parts look the same&lt;/h3&gt;&#10;&lt;p&gt;This actually is good news. It means that the tools I bought from &lt;a href="https://www.vhf.com/de-de/" 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; this time are still sharp and can be used for future set(s) of prototypes.&lt;/p&gt;&#10;&lt;p&gt;It also means that my machine did not experience step losses or mechanical issues and that the workbed surface is level. This is especially important in this case as the &lt;code&gt;90°&lt;/code&gt; engrave cutter&amp;rsquo;s engage width highly depends on workpiece surface height. With an uneven workbed, the line width would differ visibly from part to part.&lt;/p&gt;&#10;&lt;h3 id="wide-engravings-have-poorer-quality-than-fine-ones"&gt;Wide engravings have poorer quality than fine ones&lt;/h3&gt;&#10;&lt;p&gt;I&amp;rsquo;m not entirely sure about why that&amp;rsquo;s the case. I&amp;rsquo;m no metal cutting mechanic so I&amp;rsquo;m missing the professional qualification, but my guess is that this is because the engrave cutter does not have a mechanism to get rid of the chips on time, mashing it into the material with its next half-turn which creates a less smooth finish.&lt;/p&gt;&#10;&lt;p&gt;If that is the case, an additional smoothing run at a low material removal rate would solve this issue.&lt;/p&gt;&#10;&lt;h3 id="chamfering-without-pre-cut-delivers-less-smooth-results"&gt;Chamfering without pre-cut delivers less smooth results&lt;/h3&gt;&#10;&lt;p&gt;My guess is that although the symptom looks different, the root cause might be the same. I&amp;rsquo;ll try with an &lt;a href="https://blog.schallbert.de/en/cut-dibond-tests/#too-high-engrave-depth"&gt;additional smoothing run&lt;/a&gt; for the other side as well.&lt;/p&gt;&#10;&lt;p&gt;Another alternative would be to reduce the feedrate and see what happens. Maybe a balance has to be found between manufacturing time and quality?&lt;/p&gt;&#10;&lt;h3 id="i-need-to-get-locators-to-fix-workpiece-offset"&gt;I need to get locators to fix workpiece offset&lt;/h3&gt;&#10;&lt;p&gt;Obviously, I wasn&amp;rsquo;t able to reproduce part placement with help of the fence when turning the part around. Some tenths of a millimeter are enough to ruin everything.&lt;/p&gt;&#10;&lt;p&gt;I could do the following:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Plan a &lt;code&gt;Job00&lt;/code&gt; that just places locator holes in the workpiece with a diameter of &lt;code&gt;5.04mm&lt;/code&gt;. Make sure they are symmetrical so the workpiece can be flipped around the diagonal edge facing to the fence for two-sided milling, while the workpiece edge&amp;rsquo;s position remains unchanged.&lt;/li&gt;&#10;&lt;li&gt;Place the workpiece against the fence and have &lt;code&gt;Job00&lt;/code&gt; executed.&lt;/li&gt;&#10;&lt;li&gt;Remove the fence.&lt;/li&gt;&#10;&lt;li&gt;Get some 5mm register pins and put them into the vacuum table&amp;rsquo;s holes, matching the workpiece&amp;rsquo;s holes.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Or I could just cut through and manually add the chamfer on the lower side with help of a hand router. As the workpieces are quite small, I&amp;rsquo;d have to design a holder first to keep my hands safe. Maybe that&amp;rsquo;s even faster than the above approach&amp;hellip;&lt;/p&gt;&#10;&lt;h3 id="some-chips-are-stuck-down-in-the-engrave-path"&gt;Some chips are stuck down in the engrave path&lt;/h3&gt;&#10;&lt;p&gt;I don&amp;rsquo;t know why that&amp;rsquo;s the case. Will have to talk to my tool manufacturer to maybe get that sorted out.&lt;/p&gt;&#10;&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;&#10;&lt;p&gt;All in all, I can be pretty happy. The machine does what I want it to, and the overall result is OK already now, early in the prototyping phase. The composite material is nice to work with and won&amp;rsquo;t eat cutters as quickly as other materials (&lt;a href="https://blog.schallbert.de/en/cnc-router-overload/"&gt;like HPL&lt;/a&gt;). The surface finish looks really nice and its &amp;ldquo;upmarket premium&amp;rdquo; touch adds to the flair. The chips are a bit annoying, though, because they like to to stick to everything and don&amp;rsquo;t want to be vacuumed up.&lt;/p&gt;&#10;&lt;p&gt;Anyways, the quality ist not fully satisfying yet so I&amp;rsquo;ll keep adding knowledge and experience to yield some improvements in the future.&lt;/p&gt;&#10;</description></item><item><title>Github Actions: Troubleshooting</title><link>https://blog.schallbert.de/en/struggling-github-actions/</link><pubDate>Tue, 14 Feb 2023</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/struggling-github-actions/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-02-14_github_actions-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Github Actions Workflow"&#10; title="Github Actions: Troubleshooting" /&gt;&#10;&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;&#10;&lt;p&gt;Why I&amp;rsquo;m writing a short post about Github Actions? Because I wanted to use it for my latest Software-project, &lt;a href="https://blog.schallbert.de/en/projects/qr-codengrave/"&gt;QR-codengrave&lt;/a&gt;. When at some distant point in the future, I have a new computer, don&amp;rsquo;t remember which IDE I used to build and run, test, and create assets with. Or, say, I have a corrupted virtual environment or &lt;code&gt;launch.json&lt;/code&gt;, I still want to be able to deploy that application, publish bugfixes or bump a release.&lt;/p&gt;&#10;&lt;p&gt;Although that&amp;rsquo;s not the usual argument for using CI/CD - typically the first reason being that collaboration on a piece of software becomes easier - still the appeal is strong enough to try Github&amp;rsquo;s automation called &lt;a href="https://docs.github.com/en/actions" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Github Actions&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;I won&amp;rsquo;t try to run nightlys or have a continuous deployment pipeline as an end in itself, though, and only want an automated build, lint, test run, and deploy stuff when merging back to the production branch.&lt;/p&gt;&#10;&lt;h3 id="got-it-but-why-a-dedicated-post"&gt;Got it, but why a dedicated post?&lt;/h3&gt;&#10;&lt;p&gt;Because it was such a pain to get it right. I spent hours and hours pushing and hoping that, this time, the goddess of Github Actions would actually have my solution built without errors and at least run some of the tests.&lt;/p&gt;&#10;&lt;p&gt;Note: If I had been more diligent, I would have installed yet another tool &lt;a href="https://github.com/nektos/act" target="_blank" rel="noopener noreferrer" class="external-link"&gt;like this&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; to have my actions run locally, both saving time and the embarassment of tens of failed builds in a row. But I wasn&amp;rsquo;t.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-02-14_gh_actions_struggle.jpg" alt="Image: Github Action failed runs"&gt;&lt;/figure&gt;&#10;&lt;p&gt;I had quite some errors that appeared on the way of making QR-codengrave. Some I wasn&amp;rsquo;t even able to properly resolve so I had to utilize work-arounds. But let&amp;rsquo;s get to that later.&lt;/p&gt;&#10;&lt;h2 id="the-script"&gt;The script&lt;/h2&gt;&#10;&lt;p&gt;Github Actions uses &lt;code&gt;YAML&lt;/code&gt; to take orders for its pipelines. It is very well &lt;a href="https://docs.github.com/en/actions/learn-github-actions" target="_blank" rel="noopener noreferrer" class="external-link"&gt;documented&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and provides many &lt;a href="https://github.com/marketplace?type=actions" target="_blank" rel="noopener noreferrer" class="external-link"&gt;readily-working scripts&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; for most scenarios and programming languages.&lt;/p&gt;&#10;&lt;p&gt;The so-called &amp;ldquo;Workflow file&amp;rdquo; looks like this:&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-YAML" data-lang="YAML"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# This workflow will install Python dependencies, run tests and lint with a single version of Python&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;# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python&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;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;python_integrate&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;on&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;push&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;branches&lt;/span&gt;: [ &lt;span style="color:#e6db74"&gt;&amp;#34;master&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;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:#f92672"&gt;branches&lt;/span&gt;: [ &lt;span style="color:#e6db74"&gt;&amp;#34;master&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;permissions&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;contents&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;read&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;jobs&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;build&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;runs-on&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ubuntu-latest&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;steps&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;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/checkout@v3&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;Set up Python 3.10&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;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/setup-python@v3&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;with&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;python-version&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;3.10&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;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Install dependencies&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;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;&#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; python -m pip install --upgrade pip&#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; pip install flake8 pytest&#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; pip install qrcodegen # Dependency install of qrcodegen&#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; if [ -f requirements.txt ]; then pip install -r requirements.txt; fi&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;Lint with flake8&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;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;&#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; # stop the build if there are Python syntax errors or undefined names&#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; flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics&#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; # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide&#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; flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics&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;Test with pytest&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;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;GabrielBB/xvfb-action@v1 &lt;/span&gt; &lt;span style="color:#75715e"&gt;# Diverts tkinter GUI to a virtual frame buffer (VFB)&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;with&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;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;&#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; pytest&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And it does not divert too much from the standard template that I was using first. The only things I added are the run &lt;code&gt;pytest&lt;/code&gt; entry and the usage of a virtual frame buffer to cirumvent issues with my GUI which makes the tests fail when the GUI tries to fire up and there&amp;rsquo;s no screen to show it on.&lt;/p&gt;&#10;&lt;h2 id="github-action-runner-errors"&gt;Github Action runner errors&lt;/h2&gt;&#10;&lt;p&gt;The following list of fails describe the errors and resolutions that I faced until I had a stable CI at around action run #50.&lt;/p&gt;&#10;&lt;h3 id="directory-mismatch"&gt;Directory mismatch?&lt;/h3&gt;&#10;&lt;p&gt;&lt;em&gt;Output&lt;/em&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-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;opt&lt;span style="color:#f92672"&gt;/&lt;/span&gt;hostedtoolcache&lt;span style="color:#f92672"&gt;/&lt;/span&gt;Python&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;3.10.9&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;x64&lt;span style="color:#f92672"&gt;/&lt;/span&gt;lib&lt;span style="color:#f92672"&gt;/&lt;/span&gt;python3&lt;span style="color:#ae81ff"&gt;.10&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;importlib&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;__init__&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;py:&lt;span style="color:#ae81ff"&gt;126&lt;/span&gt;: &lt;span style="color:#f92672"&gt;in&lt;/span&gt; import_module&#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; _bootstrap&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_gcd_import(name[level:], package, level)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;test&lt;span style="color:#f92672"&gt;/&lt;/span&gt;test_machinify_vector&lt;span style="color:#f92672"&gt;.&lt;/span&gt;py:&lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;: &lt;span style="color:#f92672"&gt;in&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;module&lt;span style="color:#f92672"&gt;&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:#f92672"&gt;from&lt;/span&gt; bin.platform.machinify_vector &lt;span style="color:#f92672"&gt;import&lt;/span&gt; MachinifyVector, Tool, EngraveParams&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;E &lt;span style="color:#a6e22e"&gt;ModuleNotFoundError&lt;/span&gt;: No module named &lt;span style="color:#e6db74"&gt;&amp;#39;bin&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;em&gt;Reason&lt;/em&gt;:&#10;I placed my source files under &lt;code&gt;/bin&lt;/code&gt; and made it available to a local instance of PyInstaller (the tool I chose to render my python sources into an executable under Windows). Upon upload, Github Actions couldn&amp;rsquo;t relate to the paths I chose and had that error prepared for me.&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;Resolution:&lt;/em&gt;&#10;Add an empty file with name &lt;code&gt;__init__.py&lt;/code&gt; into the &lt;code&gt;/bin&lt;/code&gt; folder. This will flag anything in that folder as package, thus making it available to GH actions.&#10;So what looked like a directory mismatch actually was a package-not-found error that my IDE didn&amp;rsquo;t have as it knew which files I had created.&lt;/p&gt;&#10;&lt;h3 id="yaml-syntax-errors"&gt;YAML syntax errors&lt;/h3&gt;&#10;&lt;p&gt;This error occurs because I was trying to put two workflows into one file. Somehow Github Actions seems to only accept a single one per file.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-02-14_gh_actions_syntax.jpg" alt="Image: Github Action syntax error"&gt;&lt;/figure&gt;&#10;&lt;h3 id="path-not-found-errors"&gt;Path-not-found errors&lt;/h3&gt;&#10;&lt;p&gt;I had a lot of those and they were painful to resolve. The folder structure that I was targeting looked like:&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-YAML" data-lang="YAML"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#ae81ff"&gt;assets &lt;/span&gt; &lt;span style="color:#75715e"&gt;# images, persistence file, etc.&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;# source files&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;test &lt;/span&gt; &lt;span style="color:#75715e"&gt;# pytest files for unit and integration testing&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;dist &lt;/span&gt; &lt;span style="color:#75715e"&gt;# build artifacts&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 &lt;/span&gt; &lt;span style="color:#75715e"&gt;# build process files&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But somehow, it was super hard to make my local IDE build environment, Pyinstaller, and the Github Actions Worker and its trigger of Pyinstaller to cooperate. One of those four parties would always complain that a path is missing or something else was wrong. That&amp;rsquo;s why I decided to abandon relative paths and instead use python&amp;rsquo;s &lt;code&gt;importlib_resources&lt;/code&gt;. But in the end, this turned out to be problematic with Pyinstuller on remote Github Actions.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-02-14_gh_actions_paths.jpg" alt="Image: Github Action relative path error"&gt;&lt;/figure&gt;&#10;&lt;p&gt;What finally solved the problem was to move &lt;code&gt;assets&lt;/code&gt; into &lt;code&gt;src&lt;/code&gt;. This way, the &lt;code&gt;./&lt;/code&gt; command couldn&amp;rsquo;t go wrong anywhere and although I don&amp;rsquo;t like the construct very much, I was tired of putting more time into that for a clean fix (If you know how to handle path pointing in Python for both local and remote, let me know in the &lt;a href="https://github.com/Schallbert/schallbert.github.io/discussions" 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;).&lt;/p&gt;&#10;&lt;h3 id="tkinter-headless-testing"&gt;Tkinter headless testing&lt;/h3&gt;&#10;&lt;p&gt;Later in the development process, I decided to add some integration tests into my solution. This way I wanted to make sure that child windows would actually perform the callbacks to update the main application, and vice versa e.g. that the persisted tool list is cascaded into the tool configuration window.&lt;/p&gt;&#10;&lt;p&gt;These tests were running locally but with a disadvantage: When I simulated an error or warning case, the corresponding &lt;code&gt;messageBox&lt;/code&gt; would insist to be closed manually by the user before continuing the tests. Which was just a bit annoying for me in the IDE to do, but would just be impossible to do from within Github Action&amp;rsquo;s CI pipeline.&lt;/p&gt;&#10;&lt;p&gt;So I bit the bullet and added a wrapper class to tkinter&amp;rsquo;s &lt;code&gt;messageBox&lt;/code&gt; so I could inject a mock that wouldn&amp;rsquo;t actually trigger the popup:&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;MsgBox&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;&amp;#34;&amp;#34;Re-implementation due to testing purposes: With this trick, we are able to mock these windows&#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; so we do not have to wait for users to manually close the dialog, unblocking the application again.&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;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;showinfo&lt;/span&gt;(self, title, message):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; showinfo(title&lt;span style="color:#f92672"&gt;=&lt;/span&gt;title, message&lt;span style="color:#f92672"&gt;=&lt;/span&gt;message)&#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;error&lt;/span&gt;(self, title, message):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; showerror(title&lt;span style="color:#f92672"&gt;=&lt;/span&gt;title, message&lt;span style="color:#f92672"&gt;=&lt;/span&gt;message)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Although in theory it should be possible also to invoke the popup&amp;rsquo;s &amp;ldquo;OK button&amp;rdquo; from within the test, I wasn&amp;rsquo;t able to have that automated reliably.&lt;/p&gt;&#10;&lt;p&gt;When this worked, I pushed to Github with high expectations - and got another beautiful error:&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;_tkinter.TclError: no display name and no $DISPLAY environment variable&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;At least that was easy to understand: Tkinter just didn&amp;rsquo;t know where to draw the windows - seems legit when there&amp;rsquo;s no display connected.&lt;/p&gt;&#10;&lt;p&gt;So I searched the internet and found a single line of code that miraculously solved my problem by introducing a virtual frame buffer:&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;uses: GabrielBB/xvfb-action@v1 # Diverts tkinter GUI to a virtual frame buffer (VFB)&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;Now, on Push, Github Actions rained green ticks down on me which felt really good for a change.&lt;/p&gt;&#10;</description></item><item><title>QR-codengrave V1.1 / V1.2</title><link>https://blog.schallbert.de/en/qr-codengrave1-1/</link><pubDate>Sun, 12 Feb 2023</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/qr-codengrave1-1/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-02-12_qr-codengrave-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Splash screen of QR-codengrave V1.1"&#10; title="QR-codengrave V1.1 / V1.2" /&gt;&#10;&lt;h2 id="in-a-nutshell"&gt;In a nutshell&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Faster Engrave algorithm&lt;/li&gt;&#10;&lt;li&gt;Several bugfixes&lt;/li&gt;&#10;&lt;li&gt;Usability improvements&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="demo-video"&gt;Demo video&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;iframe&#10; src="https://makertube.net/videos/embed/jFD7nxftVHjUy6LKPCFE3N"&#10; title="QR-codengrave Quick Demo"&#10; loading="lazy"&#10; allow="autoplay; fullscreen; picture-in-picture"&#10; allowfullscreen&#10; style="width: 100%; height: auto; min-height: 315px; aspect-ratio: 16/9;"&#10; &gt;&lt;/iframe&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;QR-codengrave Quick Demo&lt;/span&gt;&lt;a href="https://makertube.net/videos/embed/jFD7nxftVHjUy6LKPCFE3N" class="attr-link" target="_blank" rel="noopener noreferrer"&gt;↗&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="version-11"&gt;Version 1.1&lt;/h2&gt;&#10;&lt;h3 id="engrave-algorithm-upgrade"&gt;Engrave algorithm upgrade&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;The algorithm now scans the lines horizontally&lt;/li&gt;&#10;&lt;li&gt;One line &amp;ldquo;Left-to-Right&amp;rdquo;, next line &amp;ldquo;Right-to-Left&amp;rdquo; etc. to reduce machine travel&lt;/li&gt;&#10;&lt;li&gt;prefers lines over dots, and longer lines over shorter ones for time optimization&lt;/li&gt;&#10;&lt;li&gt;draws lines vertically or horizontally&lt;/li&gt;&#10;&lt;li&gt;rapid move positioning now is packed into vectors that can be called to new Points(X, Y) simultaneously, not only one dimension at a time&lt;/li&gt;&#10;&lt;li&gt;New algorithm just needs 80% of the time for engraving compared to the old one&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2023-02-12_qr-codengrave_algo.jpg" alt="Image: Algorithm comparison 1.0 vs. 1.1"&gt;&lt;/figure&gt;&#10;&lt;h3 id="bugfixes"&gt;Bugfixes&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;splash image (&lt;code&gt;qruwu.jpg&lt;/code&gt;) is not shown on startup when &lt;code&gt;Persistence.dat&lt;/code&gt; is not available&lt;/li&gt;&#10;&lt;li&gt;Crash when pressing &lt;code&gt;Stop Draw&lt;/code&gt; although drawing has never been initiated before&lt;/li&gt;&#10;&lt;li&gt;Crash when adding a tool on an empty selecion list&lt;/li&gt;&#10;&lt;li&gt;Initialization of application with no tool selected disables dropdown menu&lt;/li&gt;&#10;&lt;li&gt;Fix a calculation error in QR-code size estimation&lt;/li&gt;&#10;&lt;li&gt;Fix an error pixel width calculation for tapered tools&lt;/li&gt;&#10;&lt;li&gt;Actually the currently selected tool is not shown &amp;ldquo;selected&amp;rdquo; in the dropdown on startup&lt;/li&gt;&#10;&lt;li&gt;QR-codes can grow larger than the display area&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="usability-improvements"&gt;Usability improvements&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Change default tool name from &lt;code&gt;name&lt;/code&gt; to &lt;code&gt;default&lt;/code&gt; for clarity&lt;/li&gt;&#10;&lt;li&gt;Make non-tapered (V-cut) tool the default tool for better usability&lt;/li&gt;&#10;&lt;li&gt;Setting a new XY-zero offset became more intuitive (Y down is negative, X right is positive)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="version-12"&gt;Version 1.2&lt;/h2&gt;&#10;&lt;p&gt;Minor change over 1.1, most importantly a Bugfix that could lead to tool change macro not being called.&#10;In addition, tapered tool angles are now taken into account when calculating track width and QR-code engrave size.&lt;/p&gt;&#10;&lt;h3 id="bugfixes-1"&gt;Bugfixes&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Tool select &lt;code&gt;Tx&lt;/code&gt; and tool change command &lt;code&gt;M06&lt;/code&gt; are now in the same line of the G-code to correctly trigger tool change macros&lt;/li&gt;&#10;&lt;li&gt;Actual selected tool not shown on startup, instead the first tool of the list was shown&lt;/li&gt;&#10;&lt;li&gt;QR-code drawing could grow larger than the screen. This is fixed now.&lt;/li&gt;&#10;&lt;li&gt;Crash fix when trying to update status although no engrave parameters available&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="usability-improvements-1"&gt;Usability improvements&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Add application version to G-code as comment&lt;/li&gt;&#10;&lt;li&gt;Add &lt;code&gt;qr_&lt;/code&gt; prefix to G-code save-to dialog&amp;rsquo;s default name&lt;/li&gt;&#10;&lt;li&gt;Improve job duration estimate&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="availability"&gt;Availability&lt;/h2&gt;&#10;&lt;p&gt;The latest release is available for download &lt;a href="https://github.com/Schallbert/QR-codengrave/releases" target="_blank" rel="noopener noreferrer" class="external-link"&gt;here&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, free of charge for home and private use. For more info on the project, please find the &lt;a href="https://github.com/Schallbert/QR-codengrave" target="_blank" rel="noopener noreferrer" class="external-link"&gt;readme&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and the &lt;a href="https://blog.schallbert.de/en/projects/qr-codengrave/"&gt;background information&lt;/a&gt; linked here.&lt;/p&gt;&#10;&lt;h2 id="migration-from-an-earlier-version"&gt;Migration from an earlier version&lt;/h2&gt;&#10;&lt;p&gt;It is possible to copy the persistence file to the new version&amp;rsquo;s directory. This way, saved tool lists, engrave parameters, and XY0 offsets can be ported to the new version. How it&amp;rsquo;s done:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Navigate into &lt;code&gt;src/assets&lt;/code&gt; of your QR-codengrave folder.&lt;/li&gt;&#10;&lt;li&gt;copy &lt;code&gt;Persistence.dat&lt;/code&gt; to a temporary folder or e.g. your Desktop.&lt;/li&gt;&#10;&lt;li&gt;Remove the current installation by deleting the folder.&lt;/li&gt;&#10;&lt;li&gt;Download the latest release, move it where your previous release was deployed.&lt;/li&gt;&#10;&lt;li&gt;Move the &lt;code&gt;Persistence.dat&lt;/code&gt; into &lt;code&gt;src/assets&lt;/code&gt; of your new installation and start the application.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;</description></item><item><title>Breadboard Fireplace</title><link>https://blog.schallbert.de/en/led-fireplace-diy/</link><pubDate>Sat, 10 Dec 2022</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/led-fireplace-diy/</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: A small electronics Breadboard with microcontroller, LEDs etc. on to make a fireplace simulation."&#10; title="Breadboard Fireplace" /&gt;&#10;&lt;h2 id="material-list"&gt;Material list&lt;/h2&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;Count&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;~ Cost [€]&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;LED: Warm-white, e.g. Ifwd=30mA, 3lm&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1,50&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;LED: Yellow, e.g. Ifwd=35mA, 1lm&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0,60&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;LED: Red, e.g. Ifwd=35mA, 0.4lm&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;0,30&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 Nano Atmega328&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;18&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;Breadboard e.g. 170 pins&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;0,70&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;Jumper wires, pin-pin, e.g. 5cm&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;3,30&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;In addition: Some jumper wires, Mini USB cable, powerbank / wall adapter&lt;/p&gt;&#10;&lt;h2 id="tools"&gt;Tools&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;optional: hot melt gun&lt;/li&gt;&#10;&lt;li&gt;optional: soldering iron&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="the-build"&gt;The Build&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/fireplace/fireplace_detail.jpg" alt="fireplace breadboard design detail view"&gt;&lt;/figure&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://upload.wikimedia.org/wikipedia/commons/thumb/f/f9/LED%2C_5mm%2C_green_%28en%29.svg/432px-LED%2C_5mm%2C_green_%28en%29.svg.png" alt="LED closeup"&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;LED closeup for your reference&lt;/span&gt;&lt;a&#10; href="https://commons.wikimedia.org/wiki/File:LED,_5mm,_green_%28en%29.svg#file"&#10; class="attr-link"&#10; aria-label="Attribution 1"&#10; &gt;&#10; &lt;sup class="attr-id"&gt;[1]&lt;/sup&gt;&#10; &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Connect the Arduino Nano&amp;rsquo;s right header to the left hand side of the breadboard. Make sure all pins are on level. If possible, don&amp;rsquo;t touch the pins due to risk of electrostatic discharge that may damage the device.&lt;/li&gt;&#10;&lt;li&gt;Connect the anodes (&lt;code&gt;+&lt;/code&gt;) of the LEDs to the board&amp;rsquo;s outputs &lt;code&gt;D2 through D9&lt;/code&gt;. The anode most often is the LED&amp;rsquo;s pin that doesn&amp;rsquo;t carry the chip (left in this case).&lt;/li&gt;&#10;&lt;li&gt;Place the cathodes (&lt;code&gt;-&lt;/code&gt;) of the LEDs to the right hand side of the board.&lt;/li&gt;&#10;&lt;li&gt;Place the header to the right of the LED&amp;rsquo;s cathodes.&lt;/li&gt;&#10;&lt;li&gt;To connect the cathodes, user jumper wires. Alternatively, solder the cathodes together right away.&lt;/li&gt;&#10;&lt;li&gt;Using a jumper wire, connect the &amp;ldquo;common cathode&amp;rdquo; to one of the &lt;code&gt;GND&lt;/code&gt; pins of the microcontroller board.&lt;/li&gt;&#10;&lt;li&gt;The LED anodes shouldn&amp;rsquo;t be able to touch each other. You can ensure this by placing some hot melt at their legs.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="programming"&gt;Programming&lt;/h2&gt;&#10;&lt;p&gt;The code is so simple that I can just paste it here. Copy it into your favourite IDE, program your board and you&amp;rsquo;re done!&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;Disclaimer&lt;/em&gt;: This piece of code is &amp;ldquo;quick&amp;rsquo;n&amp;rsquo;dirty&amp;rdquo; and has lots of potential for refactoring. For instance, the pin IDs are hardcoded, I&amp;rsquo;m not using &lt;code&gt;C++11&lt;/code&gt;&amp;rsquo;s range-based loop, and the speed algo will hit the low limit quite often and not show random behavior.&#10;I&amp;rsquo;m happy to hear your suggestions on my &lt;a href="https://github.com/Schallbert/schallbert.github.io/discussions" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Github discussions&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; page.&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;#include&lt;/span&gt; &lt;span style="color:#75715e"&gt;&amp;lt;Arduino.h&amp;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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#define MAXLONG 0xFFFFFFFF&#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 SPEED_HIGHLIMIT 1000 &lt;/span&gt;&lt;span style="color:#75715e"&gt;// 500Hz&#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 SPEED_LOWLIMIT 0x1FFF &lt;/span&gt;&lt;span style="color:#75715e"&gt;// 20Hz&#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;void&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;calcFlicker&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;uint32_t&lt;/span&gt; randomFlicker);&#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;writeLed&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; columnCode, &lt;span style="color:#66d9ef"&gt;uint16_t&lt;/span&gt; speed);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;uint32_t&lt;/span&gt; iRNG &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;&#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;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;randomSeed&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;analogRead&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:#a6e22e"&gt;pinMode&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, OUTPUT);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;pinMode&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;, OUTPUT);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;pinMode&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;, OUTPUT);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;pinMode&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;, OUTPUT);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;pinMode&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;6&lt;/span&gt;, OUTPUT);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;pinMode&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;, OUTPUT);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;pinMode&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;8&lt;/span&gt;, OUTPUT);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;pinMode&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;9&lt;/span&gt;, OUTPUT);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; iRNG &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;}&#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;{&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; iRNG &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (iRNG &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;(iRNG &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; &lt;span style="color:#a6e22e"&gt;calcFlicker&lt;/span&gt;(iRNG);&#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;/*&#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; This function takes a 32bit random value,&#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; and moves this value along a window of an 8bit &#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; flicker output value.&#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;*/&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;calcFlicker&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;uint32_t&lt;/span&gt; randomFlicker)&#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; currentFlicker &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt;)(randomFlicker &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0xFF&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;uint16_t&lt;/span&gt; randomSpeed &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;min&lt;/span&gt;(SPEED_LOWLIMIT, (&lt;span style="color:#66d9ef"&gt;uint16_t&lt;/span&gt;)(randomFlicker &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; SPEED_HIGHLIMIT));&#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;(&lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; i &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;; i &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;24&lt;/span&gt;; &lt;span style="color:#f92672"&gt;++&lt;/span&gt;i) &lt;span style="color:#75715e"&gt;// 24 because of 32bit length of random, may not run empty&#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;writeLed&lt;/span&gt;(currentFlicker, randomSpeed);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; randomFlicker &lt;span style="color:#f92672"&gt;=&lt;/span&gt; randomFlicker &lt;span style="color:#f92672"&gt;&amp;gt;&amp;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; } &#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;/*&#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; this function outputs a code to an LED column of 8 LEDs&#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; connected to arduino&amp;#39;s pins with a selectable delay.&#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;*/&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;writeLed&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;uint8_t&lt;/span&gt; columnCode, &lt;span style="color:#66d9ef"&gt;uint16_t&lt;/span&gt; speed)&#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;digitalWrite&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, columnCode &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0x01&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;digitalWrite&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;, columnCode &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0x02&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;digitalWrite&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;, columnCode &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0x04&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;digitalWrite&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;, columnCode &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0x08&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;digitalWrite&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;6&lt;/span&gt;, columnCode &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0x10&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;digitalWrite&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;, columnCode &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0x20&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;digitalWrite&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;8&lt;/span&gt;, columnCode &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0x40&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;digitalWrite&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;9&lt;/span&gt;, columnCode &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0x80&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;delayMicroseconds&lt;/span&gt;(speed);&#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="working"&gt;Working&lt;/h2&gt;&#10;&lt;p&gt;Once you power the device up via USB, the LED flicker sequence should start and all LEDs should light up eventually. You&amp;rsquo;ll notice how the glow calms you down. Maybe you have some twigs ore firewood at the ready and an empty space in your shelf?&#10;Take a powerbank and hide your Breadboard Fireplace below its wooden decoration. You&amp;rsquo;re done!&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://blog.schallbert.de/en/projects/fireplace/"&gt;Back to Project main page&lt;/a&gt;&lt;/p&gt;&#10;</description></item><item><title>Vacuum Pump</title><link>https://blog.schallbert.de/en/cnc-vacuum-pumps/</link><pubDate>Fri, 28 Oct 2022</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/cnc-vacuum-pumps/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-10-28_vacuumpump-thumb.jpg"&#10; class="post-cover"&#10; alt="Image of a Sidechain compressor vacuum pump"&#10; title="Vacuum Pump" /&gt;&#10;&lt;h2 id="what-is-it"&gt;What is it?&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-10-28_manometer.jpg" alt="Image: Vacuum intake manifold"&gt;&lt;/figure&gt;&#10;A vacuum pump is an appliance that is able to evacuate a vessel, i.e., to reduce the amount of gas atoms within a container. It does not seem to be possible to shrink the number of atoms within to 0, but of course this is by far not required for our application in which a so-called technical coarse vacuum is sufficient.&lt;/p&gt;&#10;&lt;p&gt;There are different types of vacuum pumps with specific properties that I&amp;rsquo;ll try to shed a light on later.&lt;/p&gt;&#10;&lt;h2 id="how-does-it-clamp-workpieces"&gt;How does it clamp workpieces?&lt;/h2&gt;&#10;&lt;p&gt;For clamping with the use of a vacuum table (see my related &lt;a href="https://blog.schallbert.de/en/why-vacuum-table/"&gt;post&lt;/a&gt;), we&amp;rsquo;re only interested in the pressure difference the vacuum pump generates versus ambient pressure which is (&lt;code&gt;103000Pa = 10.3N/cm² = 1kg/cm²&lt;/code&gt;). This is the maximum clamping force a vacuum table can generate in theory.&lt;/p&gt;&#10;&lt;h3 id="pump-capacity-vs-vacuum-pressure"&gt;Pump capacity vs. vacuum pressure&lt;/h3&gt;&#10;&lt;p&gt;The key parameters of any vacuum pump are the minimum absolute pressure they are able to draw in &lt;code&gt;hPa&lt;/code&gt; (at low or even zero throughput) and the volume of air they&amp;rsquo;re able to put through within a given time in &lt;code&gt;m³/h&lt;/code&gt; (at low or zero pressure difference).&lt;/p&gt;&#10;&lt;p&gt;This is comparable to the electrical world, where open circuit voltage (pressure difference) and short circuit current (throughput) e.g. in photovoltaic cells are given parameters to calculate an optimal point of operation.&lt;/p&gt;&#10;&lt;p&gt;Here, you can add more cells to either get more current or more voltage from the system by connecting them together in parallel or in series. For vacuum pumps, it&amp;rsquo;s often more practical to choose a more powerful motor that drives the pump than using series/parallel operation. But still, there&amp;rsquo;s a trade-off to make. Certain pump types are very well suited to generate lots of throughput, but then are lacking vacuum pressure and vice versa, pumps that have very good vacuum ratings often have considerably less throughput than other pump types with the same power level.&lt;/p&gt;&#10;&lt;p&gt;If you want both, prepare to spend a lot of money. So again, it all comes down to what you need for your application.&lt;/p&gt;&#10;&lt;h3 id="pump-characteristics"&gt;Pump characteristics&lt;/h3&gt;&#10;&lt;p&gt;Max. Volume and max. pressure difference can be entered as data points in a XY-diagram, and a line can be drawn between these two points which gives an approximation of overall pump behavior. While the position of these points largely depend on the pump&amp;rsquo;s motor power, the line&amp;rsquo;s slope is determined by pump type and design.&lt;/p&gt;&#10;&lt;p&gt;Below find a comparison of these different pump types. For this example, I arbitrarily chose a motor power of &lt;code&gt;1.5kW&lt;/code&gt; and selected some pumps with that rating of which I could find datasheets online.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-10-28_pump_type_compare.jpg" alt="Image: XY-line graph for pump types of same power"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Of course this approximation is very rough and holds true for high-throughput pumps better than for deep vacuum pump types. For the latter, the function looks more like a hyperbolic cosecant (see image below) than a linear graph due to their lower tolerance for leakage air.&lt;/p&gt;&#10;&lt;p&gt;We can use the pump&amp;rsquo;s characteristic curve the following way: When we have a given air volume demanded by our application (like a vacuum table), we can read the reachable pressure difference for this specific pump. This way, you can pre-select a pump type depending on your requirements.&lt;/p&gt;&#10;&lt;h2 id="pump-types"&gt;Pump types&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-10-28_pump_types.jpg" alt="Image: Comparison of vaccuum pump leakage behavior"&gt;&lt;/figure&gt;&#10;There are a multitude of pump types in the industry. They have different principles of operation and - of course - are suited for different applications. I&amp;rsquo;ll concentrate on the four types that are most often used for machining.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Radial blower (High Throughput)&lt;/li&gt;&#10;&lt;li&gt;Sidechannel compressor (High Throughput)&lt;/li&gt;&#10;&lt;li&gt;Piston pump (Deep vacuum)&lt;/li&gt;&#10;&lt;li&gt;rotary vane pump (Deep vacuum)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="radial-blower"&gt;Radial blower&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-10-28_radialblower.jpg" alt="Image: Radial blower symbol view"&gt;&lt;/figure&gt;&#10;Radial blowers accelerate air between their blades. Air enters the blower at the center of the fan which creates a lower pressure here than in the surrounding air. Then, it is pressed towards the outside due to centrifugal forces as the fan rotates, and is exhausted at the perimeter.&lt;/p&gt;&#10;&lt;p&gt;Radial blowers are used for many applications like vacuum cleaners, inflatable castle blowers, forced air delivery for combustion processes, ventilation systems etc. They are simple, come cheap, and are well suited for applications that require very high throughput. The downsides are high noise levels and low pressure differences relative to most other compressor types.&#10;However, if you&amp;rsquo;re working with large surface sheets or porous material, the radial blower e.g. of a forced-air cooled vacuum cleaner motor might be just the right thing for your application.&lt;/p&gt;&#10;&lt;h3 id="sidechannel-compressor"&gt;Sidechannel compressor&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-10-28_sidechaincompressor.jpg" alt="Image: Sidechain compressor symbol view"&gt;&lt;/figure&gt;&#10;Sidechannel compressors combine high capacity with medium pressure difference. They are built single or multi-stage (series circuit) and thus can favour higher capacity or pressure difference according to your needs. Like radial blowers, they interact with the medium only and thus do not have part to part friction that would require frequent maintenance. Like radial blowers, they tolerate vapours and can even be used in dusty environments.&lt;/p&gt;&#10;&lt;p&gt;If you want the sidechannel compressor to provide high pressure differences, you&amp;rsquo;ll have to buy very powerful (thus expensive) drives as their characteristic curves are shallow. Their noise can be efficiently muffled, but they often carry a higher price tag than comparable radial blowers.&lt;/p&gt;&#10;&lt;p&gt;In this list, they are &amp;ldquo;the jack of all trades&amp;rdquo;. The vacuum they provide can be upgraded to high-enough levels for most applications that don&amp;rsquo;t require extremely strong clamping and their price is moderate. Cost of operation is determined by the electrical power they consume.&lt;/p&gt;&#10;&lt;p&gt;I found this in-depth description of the sidechannel compressor helpful: &lt;a href="https://gutmbh.de/SKVM.htm" target="_blank" rel="noopener noreferrer" class="external-link"&gt;GutmbH&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="piston-pump"&gt;Piston pump&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-10-28_pistonpump.jpg" alt="Image: Piston pump symbol view"&gt;&lt;/figure&gt;&#10;Piston pumps are commonly used in compressors and vacuum pumps alike. They provide deep vacuum levels at a low capacity, don&amp;rsquo;t use much space in your shop, and are reasonably priced.&lt;/p&gt;&#10;&lt;p&gt;On the other hand, their output stream is pulsating and they tend to be noisy. They also cannot handle liquids or vapours and have higher requirements on maintenance than frictionless pumps like sidechannel compressors or radial blowers. I did not find so many notes about their usage for vacuum tables, but that does not mean they&amp;rsquo;re not suited for this specific application.&lt;/p&gt;&#10;&lt;h3 id="rotary-vane-pump"&gt;Rotary vane pump&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-10-28_rotaryvanepump.jpg" alt="Image: Rotary vane pump symbol view"&gt;&lt;/figure&gt;&#10;Rotary vane pumps come in two classes again: Dry-running and oil-lubricated.&#10;While dry-running pumps have similar pressure data at higher capacity than piston pumps with the same power rating, oil-lubricated rotary vane pumps can provide a much deeper vacuum level and even higher capacity with the same power rating. On the downside, they commonly need oil warmup and de-gasing cooldown runs, and have a higher maintenance cost due to their oil system.&lt;/p&gt;&#10;&lt;p&gt;Their principle of operation: A motor drives a slotted dish mounted excentrically within the vacuum chamber. In the slots, vanes are placed that can move freely. As the pump&amp;rsquo;s motor accelerates, the vanes are pressed against the vacuum chamber&amp;rsquo;s walls, sealing it against the other cavities. This way, compressible media can be transported only.&lt;/p&gt;&#10;&lt;p&gt;Rotary vane pumps deliver a continuous flow at low noise levels. The vanes are expendable parts that need replacement from time to time, which is especially true for the dry-running specimen. If you just compare capacity, they usually cost twice to three times as much as sidechannel compressors.&lt;/p&gt;&#10;&lt;p&gt;You should get one of theese if you often mill small workpieces at high lateral forces, e.g. mild steel. In this scenario, you can even keep your costs at bay when you&amp;rsquo;re not cutting through as in that case, leakage air is no issue of yours so you can choose a very low pump capacity.&lt;/p&gt;&#10;&lt;p&gt;If you&amp;rsquo;d like more detail, please find this well-made page (in German) from &lt;a href="https://gutmbh.de/Vakuumpumpen.htm" target="_blank" rel="noopener noreferrer" class="external-link"&gt;GutmbH&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="let-the-workpiece-determine-your-pump"&gt;Let the workpiece determine your pump&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-10-21_table_operating_principle.jpg" alt="Image: Vacuum table principle of operation - pressure difference causes downforce"&gt;&lt;/figure&gt;&#10;OK, so you chose the type and size of vacuum table you require for your jobs. Great! Now all you still need is a vacuum source and a way to feed the vacuum into your table.&lt;/p&gt;&#10;&lt;p&gt;As you can see in the figure, the downforce you get highly depends on the surface area of your workpiece. The smaller your workpiece is, the more pressure difference you require to maintain the same force - with the atmospheric pressure being one limit and the absolute vacuum the other. This makes vacuum tables both a very homogenuous but &amp;ldquo;weak&amp;rdquo; clamping method.&lt;/p&gt;&#10;&lt;p&gt;Now, what you actually want is to protect your workpiece both from being lifted off the of the machine bed by the tool, at the same time avoiding parallel shifts and rotation of your workpiece while machining. So we have to calculate the downfore created. In a second step, we want to convert that downforce into lateral forces that have to be higher than the maximum lateral force applied by your tool so your workpiece keeps its position.&lt;/p&gt;&#10;&lt;h3 id="downforce"&gt;Downforce&lt;/h3&gt;&#10;&lt;p&gt;The clamping force to your machine bed is easy to calculate:&lt;/p&gt;&#10;$$f_{clamp}= F_N = \Delta p A + F_g $$&lt;p&gt;It is measured in &lt;code&gt;Newton&lt;/code&gt; and determined by the pressure difference at the workpiece top/bottom its surface and its weight that gravity forces downwards.&lt;/p&gt;&#10;&lt;p&gt;The value you require heavily depends on tool size and geometry, workpiece material, and feed/speed.&#10;You&amp;rsquo;ll require a lot more downforce for a scenario in which you&amp;rsquo;re milling small workpieces of sturdy materials like HPL with large tool spiral angles and tool diameters at high feeds and low spindle RPM than for cutting plywood sheets with small-diameter straight flute bits at high RPM and low feed rates.&lt;/p&gt;&#10;&lt;h3 id="lateral-force"&gt;Lateral force&lt;/h3&gt;&#10;&lt;p&gt;To do the translation of downforce into lateral forces, we&amp;rsquo;ll introduce the &lt;a href="https://en.wikipedia.org/wiki/Friction#Coefficient_of_friction" target="_blank" rel="noopener noreferrer" class="external-link"&gt;coefficient of static friction&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; \(µ_s\).&lt;/p&gt;&#10;&lt;p&gt;Let&amp;rsquo;s assume you use a sheet of rubber between a metal workpiece and your vacuum table. We further assume a coefficient of static friction &lt;code&gt;µs = 0.7&lt;/code&gt; that &lt;a href="https://www.schweizer-fn.de/stoff/reibwerte/reibwerte.php" target="_blank" rel="noopener noreferrer" class="external-link"&gt;I found online&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; which seems really conservative for multiple reasons: First, it doesn&amp;rsquo;t take surface roughness of both rubber and metal into account, and, more importantly, we&amp;rsquo;re pressing the workpiece &lt;em&gt;into&lt;/em&gt; the rubber which yields a form-fit bonding that increases breakaway force a lot.&lt;/p&gt;&#10;&lt;p&gt;Anyways, all you need to do here is to take your downforce calculated earlier and multiply it with this factor:&lt;/p&gt;&#10;$$F_s = F_N µ_s$$&lt;p&gt;As you can see, there are two things you can manipulate with the same linear weighing: The material pairing of workpiece surface and machine table determine the coefficient, and the downforce created by your pump.&lt;/p&gt;&#10;&lt;h3 id="rotation"&gt;Rotation&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-10-28_rotatingworkpiece.jpg" alt="Image: Worst-case scenario for rotating a workpiece out of position"&gt;&lt;/figure&gt;&#10;This calculation becomes a bit more tricky. Interestingly, you&amp;rsquo;ll require less force to rotate a workpiece out of position than to parallel-shift it away. This is why:&lt;/p&gt;&#10;&lt;p&gt;When you rotate, say, your mobile phone on your desk around its center (touching it at its top-right corner), not all imaginable points on that phone have to move the same distance. Points closer to the center of rotation will have to move a shorter distance for the same angle of rotation than points at the tip of the phone. For a parallel shift, however, every imaginary point had to move the same distance.&lt;/p&gt;&#10;&lt;p&gt;This pheonomenon seems independent of the object&amp;rsquo;s shape once you compare travel of the outermost point of that object in relation to its center of rotation.&lt;/p&gt;&#10;&lt;p&gt;For vacuum clamping, worst-case workpiece and job combination is long, sleek workpieces that need a slot to be milled orthogonally to its longest dimension at the outer edge, creating the highest torque that might lead to rotation combined with little surface available for vacuum clamping.&lt;/p&gt;&#10;&lt;p&gt;Best-case of course are round or square workpieces where you cut close to their geometrical center.&lt;/p&gt;&#10;&lt;h2 id="which-pump-should-you-buy"&gt;Which pump should you buy?&lt;/h2&gt;&#10;&lt;p&gt;Ok, let&amp;rsquo;s sum it up:&lt;/p&gt;&#10;&lt;p&gt;It&amp;rsquo;s always a combination of workpiece material (easy/hard to mill, low/high static coefficient of friction), minimum workpiece size (the bigger, the easier it becomes to generate high clamping forces), leakage air tolerance (porous surfaces, cutting all the way through), vacuum table type and size, and milling parameters that determine which pump to buy.&lt;/p&gt;&#10;&lt;p&gt;You should collect some experience before buying a vacuum clamping solution: What is your typical workpiece size and material? Do you often do cutouts? Do you require quick execution that exposes your parts to high forces?&lt;/p&gt;&#10;&lt;h3 id="some-recommended-combinations"&gt;Some recommended combinations&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Aluminium, very small workpieces, not cutting through: Low capacity piston or rotary vane pump, grid vacuum table&lt;/li&gt;&#10;&lt;li&gt;Wood and MDF, large workpieces, cutting through a lot: radial blower or single stage sidechain compressor, porous surface table&lt;/li&gt;&#10;&lt;li&gt;HPL, small workpieces, cutting through: High capacity rotary vane pump or dual stage sidechain compressor, hole grid table, optimized CAM strategies for &amp;ldquo;late cut-throughs&amp;rdquo; at low depth increment&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="what-pump-i-chose"&gt;What pump I chose&lt;/h3&gt;&#10;&lt;p&gt;I ordered a &lt;a href="https://blog.schallbert.de/en/why-vacuum-table/#my-selection"&gt;relatively large table&lt;/a&gt; (&lt;code&gt;730 x 655 mm&lt;/code&gt;) with &lt;code&gt;0.5mm&lt;/code&gt; holes arranged in a &lt;code&gt;10mm grid&lt;/code&gt;. As I&amp;rsquo;m still testing a lot of different materials for prototyping purposes, my workpieces never look alike or have similar sizes. That&amp;rsquo;s why I had to swallow the pill of spending a lot of money on a pretty capable pump that would both do high throughput for bigger wooden sheets that I&amp;rsquo;d create cut-throughs in and deep vacuum levels for small aluminium workpieces.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-10-28_selection.jpg" alt="Image: Grid vacuum table"&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Rotary vane pump comparison (Image courtesy of Stritzelberger GmbH)&lt;/span&gt;&lt;a&#10; href="https://www.vakuumtisch.de/"&#10; class="attr-link"&#10; aria-label="Attribution 1"&#10; &gt;&#10; &lt;sup class="attr-id"&gt;[1]&lt;/sup&gt;&#10; &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;The pump I chose is a dry-running vane-type vacuum pump with a capacity of &lt;code&gt;40m³/h&lt;/code&gt; and a maximum pressure difference of &lt;code&gt;880mBar&lt;/code&gt;. Its air leakage vs. pressure diagram should look similar to the marked characteristic in the plot above. It weighs approximately &lt;code&gt;40kg&lt;/code&gt; and uses a &lt;a href="https://en.wikipedia.org/wiki/Induction_motor" target="_blank" rel="noopener noreferrer" class="external-link"&gt;3-phase induction motor&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; with a nominal power of &lt;code&gt;1.3kW&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;I am using a version where the motor is operated on a single-phase with help of a capacitor due to the fact that I do not have a three-phase outlet available. I also added a motor circuit breaker to help protect the machine against overload and overcurrent.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-10-28_my_dst40_pump.jpg" alt="Image: My DST40 dry-running vane-type vacuum pump"&gt;&lt;/figure&gt;&#10;&lt;h3 id="is-it-loud"&gt;Is it loud?&lt;/h3&gt;&#10;&lt;p&gt;It depends on how you define loud, of course. You can compare its &lt;code&gt;68dB(A)&lt;/code&gt; noise level - measured at a distance of 1 meter - with a typical vacuum cleaner, just that it sounds pretty different. When the CNC machine operates, e.g. running through material with a fairly big endmill, it is still quiet and you won&amp;rsquo;t realize the pump is on.&lt;/p&gt;&#10;&lt;p&gt;I even made a short video for you to view and hear for yourself:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;iframe&#10; src="https://makertube.net/videos/embed/9CsPToFRYf3R7S6FC87gRZ"&#10; title="Rotary Vane Vacuum Pump - demonstration"&#10; loading="lazy"&#10; allow="autoplay; fullscreen; picture-in-picture"&#10; allowfullscreen&#10; style="width: 100%; height: auto; min-height: 315px; aspect-ratio: 16/9;"&#10; &gt;&lt;/iframe&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Rotary Vane Vacuum Pump - demonstration&lt;/span&gt;&lt;a href="https://makertube.net/videos/embed/9CsPToFRYf3R7S6FC87gRZ" class="attr-link" target="_blank" rel="noopener noreferrer"&gt;↗&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;</description></item><item><title>CNC Vacuum Table</title><link>https://blog.schallbert.de/en/why-vacuum-table/</link><pubDate>Fri, 21 Oct 2022</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/why-vacuum-table/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-10-21_table_gr_thumb.jpg"&#10; class="post-cover"&#10; alt="Image of my hole grid vacuum table"&#10; title="CNC Vacuum Table" /&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-10-21_table_operating_principle.jpg" alt="Image: Vacuum table principle of operation - pressure difference causes downforce"&gt;&lt;/figure&gt;&#10;Let&amp;rsquo;s assume you clicked this article on purpose and didn&amp;rsquo;t just &amp;ldquo;surf the internet&amp;rdquo;. So you already know what a vacuum table is (If not, continue reading &lt;a href="https://en.wikipedia.org/wiki/Vacuum_table" target="_blank" rel="noopener noreferrer" class="external-link"&gt;here&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;). Its principle of operation is depicted in the figure for your reference. You probably also know there are multiple types of tables serving different purposes. And you maybe even know that you want one.&lt;/p&gt;&#10;&lt;p&gt;Great! This article both describes my journey towards acquiring this piece of tech and also will try to help you select the right one. Let&amp;rsquo;s start with a little interview.&lt;/p&gt;&#10;&lt;h2 id="why-do-you-want-a-vacuum-table"&gt;Why do you want a vacuum table?&lt;/h2&gt;&#10;&lt;p&gt;This is &lt;em&gt;the&lt;/em&gt; most important question.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;🤩 Do you want it because it&amp;rsquo;s expensive and looks great?&lt;/li&gt;&#10;&lt;li&gt;😪 Or because you&amp;rsquo;re tired of re-adjusting clamps?&lt;/li&gt;&#10;&lt;li&gt;😖 Or because you broke a lot of endmills running through the former by accident?&lt;/li&gt;&#10;&lt;li&gt;😨 Or rather because you experienced that workpieces tend to learn how to fly when the adhesive force of your tape is just not high enough?&lt;/li&gt;&#10;&lt;li&gt;😎 Perhaps because you`re going professional and you want to cut preparation and production time?&lt;/li&gt;&#10;&lt;li&gt;😕 Or maybe because the work results are not as good as they could (or should) be?&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="my-reasons-quality-and-speed"&gt;My Reasons: Quality and Speed&lt;/h3&gt;&#10;&lt;p&gt;I&amp;rsquo;d go for the latter. I have the following issues I hope a vacuum table can fix:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;When engraving coated materials (Dibond, HPL etc.), slight Z0 surface height differences ruin the work, especially on bigger workpieces&lt;/li&gt;&#10;&lt;li&gt;Cutouts from a bigger sheet have to have taps to keep them from slinging away. These taps are cut away later but are still visible on the end product.&lt;/li&gt;&#10;&lt;li&gt;When I don&amp;rsquo;t want to use taps, I have to leave an onion skin at the lower layer. With wood, this skin has to be thicker and I need a second job for chamfering which adds time expenditure.&lt;/li&gt;&#10;&lt;li&gt;Some sheets (especially wood) are concave. Thus, when trying to go all the way through, some parts are fine and others are not fully cut.&lt;/li&gt;&#10;&lt;li&gt;My machine &lt;a href="https://blog.schallbert.de/en/cnc-vibrates/"&gt;suffers from vibrations&lt;/a&gt;, especially when using 2-flute wood roughers and high Z+ depth per pass at aggressive feeds. This might be because I use just a couple of clamps to save time and to keep travel paths clear.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="which-type-of-table-do-you-need"&gt;Which type of table do you need?&lt;/h2&gt;&#10;&lt;p&gt;Ok, you know why you want one, now we should find out which type is the correct one for your jobs. Note that vacuum tables are most suitable when you&amp;rsquo;re milling not too flexible sheet material as the table&amp;rsquo;s clamping force depends on the surface that is in touch with it.&lt;/p&gt;&#10;&lt;p&gt;There are three families of vacuum tables:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Grid tables&lt;/li&gt;&#10;&lt;li&gt;Hole grid tables&lt;/li&gt;&#10;&lt;li&gt;Porous surface tables&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="grid-tables"&gt;Grid tables&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-10-21_table_r.jpg" alt="Image: Grid vacuum table"&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;A grid vacuum table (Image courtesy of Stritzelberger GmbH)&lt;/span&gt;&lt;a&#10; href="https://www.vakuumtisch.de/Grid-vacuum-table-R-Series"&#10; class="attr-link"&#10; aria-label="Attribution 1"&#10; &gt;&#10; &lt;sup class="attr-id"&gt;[1]&lt;/sup&gt;&#10; &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;Grid tables have a single (and often centrally placed) suction hole. Their vaccuum area is defined by a (foam) rubber band that can be placed on the grid within the outline of the workpiece. As the rubber band is compressed by the workpiece&amp;rsquo;s downforce created by the vacuum, the seal is airtight and just very few leakage air has to be evacuated. That&amp;rsquo;s why they have low requirements on the vacuum pump&amp;rsquo;s required capacity. They can also be used to clamp materials with rough surfaces due to the rubber band, e.g. saw-cut wood. Grid tables&amp;rsquo; designs are often simple and thus affordable.&lt;/p&gt;&#10;&lt;p&gt;Altough they really shine here, there are two possibly vital disadvantages: First, when you&amp;rsquo;re doing singletons, you&amp;rsquo;ll have to place the rubber band anew for each workpiece. So your XY Zero changes from part to part as most often the workpiece has to be placed centrally on the table. More sophisticated grid tables may have multiple suction holes, and the active one can be selected by the user. They may even offer fences and excenter stops to reduce set-up time. Still, you might not be able to cut any form you like as your limit is the grid.&lt;/p&gt;&#10;&lt;p&gt;Second and more important, you run into issues once your part has to be cut &amp;ldquo;through&amp;rdquo;. This will immediately make the vacuum collapse and the pump will be overwhelmed with leakage unless you take precautions (build adapter plates or underlay fences etc.)&lt;/p&gt;&#10;&lt;p&gt;In short:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Grid tables work very well if you don&amp;rsquo;t cut through your workpieces.&lt;/li&gt;&#10;&lt;li&gt;Grid tables have lower requirements on the vacuum pump and can even hold parts with rough or slightly uneven surfaces.&lt;/li&gt;&#10;&lt;li&gt;Grid tables are cost effective but may require additional care when zeroing in workpieces with different dimensions.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="hole-grid-tables"&gt;Hole grid tables&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-10-21_table_seal.jpg" alt="Image: Hole grid vacuum table"&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;A hole grid vacuum table (Image courtesy of Stritzelberger GmbH)&lt;/span&gt;&lt;a&#10; href="https://www.vakuumtisch.de/Rastervakuumtisch-R-Serie"&#10; class="attr-link"&#10; aria-label="Attribution 2"&#10; &gt;&#10; &lt;sup class="attr-id"&gt;[2]&lt;/sup&gt;&#10; &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;Hole grid tables have - as the name suggests - blind holes that are aligned on a grid. Within these holes, there&amp;rsquo;s a tiny suction hole that&amp;rsquo;s size matches the material&amp;rsquo;s properties you&amp;rsquo;re machining predominantly - tiny little holes for smooth or polished surfaces like face milled metal, and still tiny but bigger ones e.g. for hard wood and other materials that may have higher air leakages. They often have switchable vacuum chambers below these holes so that the area of where the vacuum is applied can be changed. Uncovered area around the workpiece can be covered with a foil to raise vacuum forces and efficiency. These tables often consist of multiple parts, e.g. a bottom layer for mounting to the machine bed, a middle layer to embed evacuation ducts and the top layer carrying the hole grid.&lt;/p&gt;&#10;&lt;p&gt;These tables can clamp workpiece of any shape that have flat bottom surfaces. When your workpieces are really small, of course, the clamping force will diminish - that&amp;rsquo;s physics. The small suction hole allows for through-cuts: Affected holes do loose their capability to hold the workpiece in place on one hand; but nearby holes on the grid are merely affected as air leakage is limited by the &amp;ldquo;lost&amp;rdquo; suction hole&amp;rsquo;s diameter even at high pressure differences.&#10;Aligning the workpiece is even simpler with hole grid tables as their grid&amp;rsquo;s holes can be used to hold locator pins that build a fence.&lt;/p&gt;&#10;&lt;p&gt;Their disadvantages: These tables tend to be more expensive as their design is more complex. They often have multiple vacuum circuits and thus require more connections to your vacuum infrastructure (that have to be kept leak-free). Their tolerance of leakage air and lateral inflow means that your vacuum pump requires to have a higher capacity.&lt;/p&gt;&#10;&lt;p&gt;In short:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Hole grid tables support any sheet shape as long as its surface is flat and big enough to meet your clamping requirements.&lt;/li&gt;&#10;&lt;li&gt;Hole grid tables tolerate through-cuts at the cost of higher requirements on pump capacity.&lt;/li&gt;&#10;&lt;li&gt;Hole grid tables make workpiece alignment a cake walk.&lt;/li&gt;&#10;&lt;li&gt;Hole grid tables are complex and often expensive.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="porous-surface-tables"&gt;Porous surface tables&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-10-21_table_porous.jpg" alt="Image: Porous surface table, MDF top layer"&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;A Porous surface table (Image courtesy of &amp;#39;Jens´ Vakuumtische&amp;#39;)&lt;/span&gt;&lt;a&#10; href="https://hobbyline.info/forum/index.php?thread/366-neue-erkenntnisse-von-meinem-vakuumtisch/&amp;amp;pageNo=1"&#10; class="attr-link"&#10; aria-label="Attribution 3"&#10; &gt;&#10; &lt;sup class="attr-id"&gt;[3]&lt;/sup&gt;&#10; &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;These special kind of tables do not need holes or channels on their surface at all - their top surface, instead, is air-permeable. I&amp;rsquo;ve seen specimen where the top layer is made of MDF so actually it&amp;rsquo;s not a big problem if you accidentally got Z0 wrong - just place a new sheet of MDF on top and off you go again.&lt;/p&gt;&#10;&lt;p&gt;The tables are lightweight and - with a lot of knowledge about materials science and thin-wall milling - relatively easy to construct as a DIY project. There are many examples out there on the internet.&#10;They work best with a high-volume blower and thus often do not support large pressure differences - so as long as your workpieces are not tiny, that&amp;rsquo;s not an issue. The tables are suitable for cutting all the way through as well. They are cheap to buy and to upgrade with additional features, and a vacuum cleaner with forced cooling might already be enough to power it up.&lt;/p&gt;&#10;&lt;p&gt;The downsides: Might be subject to vibrations due to their low weight, best not to be operated with cooling liquids, unfavorable static friction coefficient with most pairing materials due to MDF (thus more surface needed to protect from slipping).&lt;/p&gt;&#10;&lt;p&gt;In short:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Porous surface tables are cheaper to have and to operate.&lt;/li&gt;&#10;&lt;li&gt;Porous surface tables are well suited for big workpieces and materials that don&amp;rsquo;t require cooling liquids.&lt;/li&gt;&#10;&lt;li&gt;Porous surface tables may not be perfect for high precision milling.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="what-all-these-types-have-in-common"&gt;What all these types have in common&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-10-21_table_vacuum_principle.jpg" alt="Image: How surface irregularities help keep pressure differences homogenous"&gt;&lt;/figure&gt;&#10;Common to all these types is, that the downforce can safely be calculated the same way for a given vacuum pressure applied to &lt;em&gt;your workpiece&lt;/em&gt; (not necessarily identical with what you see on your gauge!). No matter if you have a grid, a hole grid table or a porous surface table, you can always calculate the full surface of your workpiece (and not just the sum of holes&amp;rsquo; area within the workpiece&amp;rsquo;s outer bounds). This is because microscopic surface irregularities of both table and workpiece material allow a lateral airflow into the suction area of your table, making pressure differences between atmosphere and vacuum pump side homogenous.&lt;/p&gt;&#10;&lt;p&gt;On the other hand, this is also true for your workpiece&amp;rsquo;s outline, so there will always be a little leakage air in your system with the side effect that clamping force deteriorates the more you approach the peripheral zone.&lt;/p&gt;&#10;&lt;p&gt;That said, cutting all the way through your material will create extra peripheral zones that can, when situated too close to each other, even have zero clamping force left.&lt;/p&gt;&#10;&lt;h2 id="what-else-do-you-need"&gt;What else do you need?&lt;/h2&gt;&#10;&lt;p&gt;Well, there&amp;rsquo;s a couple of things you do require. A vacuum pump, for instance, that supplies your table. But you&amp;rsquo;re in luck, I wrote &lt;a href="https://blog.schallbert.de/en/cnc-vacuum-pumps/"&gt;an entire post&lt;/a&gt; on that topic as well. But there&amp;rsquo;s more, actually:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Fasteners, washers, nuts to attach the table to your machine bed&lt;/li&gt;&#10;&lt;li&gt;A vacuum manifold that may allow you to select chambers within your table, preferably providing a vacuum gauge to check pressure levels&lt;/li&gt;&#10;&lt;li&gt;Vacuum hose(s) to connect the table to your manifold&lt;/li&gt;&#10;&lt;li&gt;Another vacuum hose that connects the manifold to your pump&lt;/li&gt;&#10;&lt;li&gt;A pump motor switch and motor circuit breaker&lt;/li&gt;&#10;&lt;li&gt;Vacuum fleece or rubber mat (hole grid tables only)&lt;/li&gt;&#10;&lt;li&gt;Vacuum gasket thread (grid tables only)&lt;/li&gt;&#10;&lt;li&gt;porous spoilboard (porous tables only)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="conclusion"&gt;Conclusion&lt;/h3&gt;&#10;&lt;p&gt;All in all, it will cost you a lot of your time and money to get and setup a vacuum table. So you should have a couple of good arguments to go through that effort. If so, chances are high you&amp;rsquo;ll be rewarded with quicker execution, better quality, and shorter job setup times.&lt;/p&gt;&#10;&lt;h2 id="my-selection"&gt;My selection&lt;/h2&gt;&#10;&lt;p&gt;I exclusively work with sheet material. I process wood, high pressure laminates, acrylic, MDF boards, and often have to cut out parts from a bigger sheet or use nesting for many different parts. This is why a grid table wouldn&amp;rsquo;t work for me very well.&lt;/p&gt;&#10;&lt;p&gt;Due to the vibration issues I have &lt;a href="https://blog.schallbert.de/en/projects/cnc_spoilboard/"&gt;with my current setup&lt;/a&gt; lacking enough weight and the fact that a ready-made vacuum table didn&amp;rsquo;t exist for the dimensions of my machine, I didn&amp;rsquo;t choose a porous surface table either. This would have been the most cost-effective solution, though.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-10-21_bl0607_dwg.jpg" alt="Image: Creating a drawing of my machine bed for the vacuum table manufacturer"&gt;&lt;/figure&gt;&#10;Instead, I contacted a vacuum table manufacturer to buy a couple of small hole-grid tables that I could individually switch on and off depending on workpiece size and pressure that I&amp;rsquo;d require for secure clamping. Well. I was told that a much simpler and better solution would be to buy one of their even heavier aluminium tables. Less worrying to get multiple tables supplied with airtight suction ducts, valves, connectors. Not having to access both sides of the milling machine with ducts. Not having to adjust the tables to sit together tightly or face mill for a level surface. And by the way, they&amp;rsquo;d create a customized design so it would perfectly fit my machine&amp;hellip;&lt;/p&gt;&#10;&lt;p&gt;What can I say. I scraped money together for this much more elegant solution. I made a drawing of my machine bed. Just a couple of days later I got a sketch back with an initial design (this one is still missing additional holes for clamping vises, and the grid is less dense to keep processing speed high). I gave my OK for production.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-10-21_custom_table.jpg" alt="Image: Custom designed vacuum table to fit my machine"&gt;&lt;/figure&gt;&#10;&lt;h3 id="table-delivered"&gt;Table delivered&lt;/h3&gt;&#10;&lt;p&gt;The manufacturer is located not too far from where I&amp;rsquo;m at home. So I went there and collected it myself. When I had unmounted my old spoilboard, I thought it would be a good idea to have some heavy foam acoustic absorption panels placed in the machine frame to dampen noise and resonation from below the table.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-10-21_table_mounting.jpg" alt="Image: Vacuum table mounting"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Only afterwards, I bolted the new vacuum table to my machine with help of M6 fasteners and hammernuts. After that it took me multiple hours to get a more or less even surface. As the machine frame&amp;rsquo;s tolerance is relatively high, I initially had deviations of &lt;code&gt;3/10mm&lt;/code&gt; over the vacuum table&amp;rsquo;s surface 😓. With the help of some paper and thin cardbord, I was able to smoothen out the roughest dips, but I could still measure a deviation of &lt;code&gt;15/100mm&lt;/code&gt;.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;iframe&#10; src="https://makertube.net/videos/embed/8qP4FeBN2RYAVxkB4SWPHi"&#10; title="CNC Z-axis bed leveling check"&#10; loading="lazy"&#10; allow="autoplay; fullscreen; picture-in-picture"&#10; allowfullscreen&#10; style="width: 100%; height: auto; min-height: 315px; aspect-ratio: 16/9;"&#10; &gt;&lt;/iframe&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;CNC Z-axis bed leveling check&lt;/span&gt;&lt;a href="https://makertube.net/videos/embed/8qP4FeBN2RYAVxkB4SWPHi" class="attr-link" target="_blank" rel="noopener noreferrer"&gt;↗&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;To get to an optimal solution, there would be three options:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Surface milling of the whole table&lt;/li&gt;&#10;&lt;li&gt;Unmounting the table, adding thin plastic foil where needed, remounting, adding additional foil/paper where needed&amp;hellip;&lt;/li&gt;&#10;&lt;li&gt;Unmounting the table, loosen the machine frame and get to the root: the frame&amp;rsquo;s sides are rotated towards the inner just a tiny bit, and the front cross-beam seems to be bent upwards just a little, so it should be lowered by less than 1/10mm.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;For the time being, any of these options seemed too much work as I don&amp;rsquo;t know yet if I need it to be that exact. So I continued by connecting the vacuum hoses.&lt;/p&gt;&#10;&lt;h3 id="connecting-the-manifold"&gt;Connecting the manifold&lt;/h3&gt;&#10;&lt;p&gt;While happily cutting the hoses that would connect the vacuum table to the manifold, I thought &amp;ldquo;Wait a second - wouldn&amp;rsquo;t it be better to have the 9mm hoses as short as possible and rather use a longer 19mm pump connector hose for less flow resistance?&amp;rdquo; I stopped work and did some calculations:&lt;/p&gt;&#10;&lt;p&gt;My table has ~4900 holes of &lt;code&gt;0.5mm&lt;/code&gt; in diameter. Its combined cross section is \(A_{table}=962mm^2\), which is equivalent to a hole with &lt;code&gt;35mm&lt;/code&gt; in diameter (differences in flow resistance ignored).&lt;/p&gt;&#10;&lt;p&gt;The seven connecting hoses have a diameter of &lt;code&gt;9mm&lt;/code&gt;. Together, their surface is just \(A_{hose}=445mm^2\), equivalent single diameter of &lt;code&gt;24mm&lt;/code&gt; which is less than half than the table&amp;rsquo;s flow section.&lt;/p&gt;&#10;&lt;p&gt;For the pump hose and its diameter of &lt;code&gt;18mm&lt;/code&gt; it gets even worse: \(A_{pump}=254mm\) is less than 30% of the table&amp;rsquo;s capacity.&lt;/p&gt;&#10;&lt;p&gt;So when I assume an even air leakage over the seven chambers of my table, it makes most sense to have the pump hose as short as possible as the flow speed is highest here and so are the losses. For a scenario where just one of the table&amp;rsquo;s section is active and there&amp;rsquo;s a lot of leakage, obviously the &lt;code&gt;9mm&lt;/code&gt; hose should be as short as possible.&lt;/p&gt;&#10;&lt;p&gt;I decided instead to never allow full leakage so I could keep my preferred, comfortable location of where the manifold is mounted and just completed the wiring. 😉 By the way, the first part I manufactured on the new table was the holder for the manifold you can see in the image below.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-10-21_table_mounted.jpg" alt="Image: Vacuum table mounted"&gt;&lt;/figure&gt;&#10;</description></item><item><title>Estlcam: Negative Carves</title><link>https://blog.schallbert.de/en/negative-carving-with-estlcam/</link><pubDate>Tue, 27 Sep 2022</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/negative-carving-with-estlcam/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-09-27_carve_results-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: A negative carve in wood"&#10; title="Estlcam: Negative Carves" /&gt;&#10;&lt;p&gt;I planned to have a wooden sign carved for a colleague of mine. But I wanted it to be inverted, i.e. the letters should stand from of the background. As there are multiple ways to accomplish that in Estlcam (a Computer Aided Machining tool) but not all are quick or easy to find, I&amp;rsquo;ll post a quick step-by-step guide here.&lt;/p&gt;&#10;&lt;h3 id="1-import-your-frame"&gt;1. Import your frame&lt;/h3&gt;&#10;&lt;p&gt;Open a drawing, e.g. a &lt;code&gt;DXF&lt;/code&gt; of the frame of your sign. I designed one with rounded corners and an outer border in CAD before. If you&amp;rsquo;re carving in wood, make sure that your carve depth is not higher than the width of your frame - otherwise the frame might not have enough stability.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-09-27_frame.jpg" alt="Image: Frame for my sign"&gt;&lt;/figure&gt;&#10;&lt;h3 id="2-design-your-text"&gt;2. Design your text&lt;/h3&gt;&#10;&lt;p&gt;Add the text you want the sign to &amp;ldquo;wear&amp;rdquo;. Update the text field width so it fits your sign. Then select the text in your text field and choose a font. Finally, choose a text height. Depending on the cutter you&amp;rsquo;re planning to use, select the carve depth. Cutters with sharper angles may have deeper carves than e.g. the classic &lt;code&gt;90°&lt;/code&gt; V-cutter. For my &lt;code&gt;12mm&lt;/code&gt; sheet, I chose a carve depth of &lt;code&gt;4.5mm&lt;/code&gt;. Then specify a maximum carve width (how big the &amp;ldquo;valleys&amp;rdquo; in your sign should be). I wanted a flat valley, so I entered a large value here. Last but not least, press the &lt;code&gt;select frame&lt;/code&gt; button to choose have the limits of your carve defined as the inner bound of your frame.&lt;/p&gt;&#10;&lt;p&gt;Leave the window open, please.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-09-27_carve_text.jpg" alt="Image: Settings for the text to carve"&gt;&lt;/figure&gt;&#10;&lt;h3 id="3-tool-selection"&gt;3. Tool selection&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-09-27_carve_preview1.jpg" alt="Image: Carve preview 1"&gt;&lt;/figure&gt;&#10;Select the tool you want to do the carve with by klicking the appropriate line in your tool table. Do not yet select the &lt;code&gt;disassemble Text object&lt;/code&gt; checkbox yet so you can still make adjustments. Click &lt;code&gt;OK&lt;/code&gt; to close the window. Hit &lt;code&gt;Preview&lt;/code&gt; to get a feeling for how the carve will look like. See the parallel lines, sitting tightly together? This reflects the parameter &lt;code&gt;clearing feed&lt;/code&gt; of the cutter in the details view of the tool table. Make sure that it&amp;rsquo;s at or below &lt;code&gt;5%&lt;/code&gt; for a not too coarse result.&lt;/p&gt;&#10;&lt;p&gt;Anyways, you will not get optimal results yet because there&amp;rsquo;s just one tool for the whole job although the &amp;ldquo;valleys&amp;rdquo; would much better be cut with a standard cutter that has a flat face.&lt;/p&gt;&#10;&lt;h3 id="4-clearing"&gt;4. Clearing&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-09-27_carve_clearing_tool.jpg" alt="Image: Carve preview 1"&gt;&lt;/figure&gt;&#10;Save your project now - you cannot edit the text or its carve parameters anymore after the next step.&#10;To get the clearing feed cutter in place, select the carve toolpath again and tick the box &lt;code&gt;disassemble Text object&lt;/code&gt;. After pressing &lt;code&gt;OK&lt;/code&gt; again, click the toolpath. You&amp;rsquo;ll now see the tool config for the carve. Add a clearing tool. Ideally, its diameter is smaller than the distance between letters and frame so you can keep the valley&amp;rsquo;s surface as smooth as possible (Note: I didn&amp;rsquo;t do that so there are some residual rough spots on my example workpiece). Maybe, also reduce clearing feed of that cutter for this job so the surface quality will become even better - values of 20-40% return very good results.&lt;/p&gt;&#10;&lt;h3 id="5-preview-number-two"&gt;5. Preview Number two&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-09-27_carve_preview2.jpg" alt="Image: Carve preview 2"&gt;&lt;/figure&gt;&#10;OK, now you should be all set. Check paths and tool changes in the preview. Look how your manufacturing time decreased as well 😉. One more tip: Under &lt;code&gt;Settings&lt;/code&gt;, reduce the safety height of your cutter to a minimum while making sure you don&amp;rsquo;t crash into clamps. Don&amp;rsquo;t worry if your sign is not legible anymore in the preview. That&amp;rsquo;s normal as it just represents the tool paths.&lt;/p&gt;&#10;&lt;p&gt;Re-check your feeds and speeds for the tools you&amp;rsquo;ll be using. When your carves are not very deep, there&amp;rsquo;s probably headroom for increased values here.&lt;/p&gt;&#10;&lt;h3 id="engrave"&gt;6. Hit the CNC&lt;/h3&gt;&#10;&lt;p&gt;Export the g-code file from Estlcam and have your CNC build your inverse carve sign. I hope you&amp;rsquo;re happy with the results!&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-09-27_carve_results.jpg" alt="Image: The completed sign"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;</description></item><item><title>AnywhereAmps Alpha</title><link>https://blog.schallbert.de/en/anywhereamps-diy/</link><pubDate>Sat, 10 Sep 2022</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/anywhereamps-diy/</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="AnywhereAmps Alpha&amp;#39;s insides"&#10; title="AnywhereAmps Alpha" /&gt;&#10;&lt;h2 id="plans"&gt;Plans&lt;/h2&gt;&#10;&lt;p&gt;{% include gallery id=&amp;ldquo;gallery&amp;rdquo; caption=&amp;ldquo;Plans for &lt;strong&gt;AnywhereAmps Alpha&lt;/strong&gt;. Click to enlarge.&amp;rdquo; %}&lt;/p&gt;&#10;&lt;h2 id="lists"&gt;Lists&lt;/h2&gt;&#10;&lt;h3 id="tools"&gt;Tools&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;Tools needed&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;Power drill&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;Router, compasses mount&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;Multi-tool: Sanding, Wood cut, cutter head&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;hand-held or table circular saw with guard rail&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;Hand saw, fine&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;hole cutter d&amp;gt;=100mm&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;Chisel, 12mm&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;Hammer&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;Screwdrivers, bits&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;Wood drills: 3mm, 4, 5, 6, 8, 10, 12mm, counterbore 45°, Forstner 30mm&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;Steel drills: 4.3mm, counterbore 45°&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;Cutter heads: 8mm straight, 12mm straight, 45°/d12mm optional&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;Soldering iron, solder, shrinking tube, hot melt, 2m of 0.75mm² cable etc.&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/wood_tools.jpg" alt="wood carving tools"&gt;&lt;/figure&gt;&#10;&lt;h3 id="material-list"&gt;Material list&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;Item&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Size [mm]&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Count&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;~ Cost [€]&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;Shell: Poplar Multiplex&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;12&amp;quot; x 6&amp;quot; x 7mm&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;62&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;Top/Bottom: Poplar Multiplex&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;12&amp;quot; x 12mm&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;8&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;Speaker adapter plates: Poplar Multiplex&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;L 300 B 150 W 12&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;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;Amp holder plate: Poplar Multiplex&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;L 160 B 95 W 12&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;1&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;Strengthening &amp;amp; Mounts: Birch Multiplex&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;L 220 B 30 W 12&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;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;Screws (preferably Torx)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;12 x 3&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;20&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;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;Screws (Battery holder, handlebar)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;16 x 3&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;12&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;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;Screws &amp;amp; nuts, PCB holders, washers&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;15 x M4&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;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;Foam Seal&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2000 x 9 x 3&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;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;1/4&amp;quot; Input Jack&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;50 x 12&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;3&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;Power switch with LED, 250VAC 4A&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&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;3&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;Potentiometer 250k log 6mm axis + Volume knob&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;22mm diameter&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;5&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;9V Output Barrel Jack&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5.6 brl, 2.4pin&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;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;Fan grill (protective speaker cover)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;120 x 120&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;1&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;&amp;ldquo;I-Type&amp;rdquo; Foam Seal (Speaker)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;600 x 9 x 2&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;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;Aluminium Enclosure&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;143 x 72 x 43&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;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Class-D power amp board, e.g. TPA 3110&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;70 x 100 + 22&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;16&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;Piezo Tweeter, e.g. Monacor MPT-005&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;85 x 85 x 72&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;12&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;4&amp;quot; Woofer/Mid speaker, e.g. Visaton KT100 V 4Ohm&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;99 x 99 x 52&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;25&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;Battery, 10V - 18V&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;What you prefer&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;?&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;Electronics parts for Circuit Board, fuse&amp;hellip;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&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;10&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;Spare wood e.g. as mask, template, helper&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;e.g. 300 x 300&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;0&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="electronics-parts-on-the-circuit-board"&gt;Electronics parts on the circuit board&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;ID&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Item&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Type&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Value&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;R1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Resistor&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0207&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;100k&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;C1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Elko&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;RM=2 D=5&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1 µF&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;C2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Elko&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;RM=2 D=5&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;4.7 µF&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;R3&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Resistor&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0207&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;10k&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;R2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Resistor&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0207&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;100k&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;-&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Header Male&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2-pin&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;-&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;-&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Header Male&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;4-pin&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;-&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;-&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Header Male&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2-pin&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;-&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;C1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Elko&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;RM=2 D=5&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2.2 µF&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;R4&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Resistor&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0207&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;10k&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;IC1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;NE5532&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;DIP-8&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;-&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="jumper-wires"&gt;Jumper wires&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;From&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;To&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;length [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;(11/1)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;(11/2)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2.54&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;(10/3)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;(10/1)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5.08&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;(14/6)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;(14/2)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;10.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;(8/1)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;(8/6)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;12.7&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;(9/2)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;(9/5)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;7.62&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/2)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;(3/0)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5.08&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;(22/0)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;(22/1)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2.54&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;hole (x/y)&lt;/p&gt;&#10;&lt;h2 id="the-build"&gt;The Build&lt;/h2&gt;&#10;&lt;h3 id="cut-holes"&gt;Cut holes&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/adapterboard_before.jpg" alt="Image: Speaker adapter plates before cut"&gt;&lt;/figure&gt;&#10;&lt;strong&gt;Warning&lt;/strong&gt; When using the hole cutter in a hand tool, be very careful - &lt;em&gt;I think it is one of the more dangerous tools.&lt;/em&gt; It is best applied gently with very low pressure and relatively high speed of &lt;code&gt;&amp;gt;300rpm&lt;/code&gt;. Use a torque support and your drill&amp;rsquo;s torque limiter. Make sure the cutter&amp;rsquo;s heads are properly fastened and the workpiece is very well secured. Prepare for sudden kickbacks and mind the high intertia torque of the heavy tool.&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Prepare the board for the speaker adapter plates as shown &lt;a href="https://blog.schallbert.de/en/anywhereamps-diy/#plans"&gt;in &amp;ldquo;Plans&amp;rdquo;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Prepare the shell: Mark the middle of its height and draw a helping line on the outside. You might want to use a scale tape to account for the curvature. Then mark the three hole centers. I propose a &lt;code&gt;125mm&lt;/code&gt; geodesic curve between them.&lt;/li&gt;&#10;&lt;li&gt;Prepare the top circle for the controls panel cutout.&lt;/li&gt;&#10;&lt;li&gt;Choose a hole diameter of &lt;code&gt;75mm&lt;/code&gt; on your hole cutter.&lt;/li&gt;&#10;&lt;li&gt;Cut two holes for the tweeters into the adapter board. If you flip the board when you&amp;rsquo;re half through, the quality of the hole&amp;rsquo;s edges will be weigh better.&lt;/li&gt;&#10;&lt;li&gt;Now cut the two holes into the shell. Be very careful here, you can easily kill the inner surface if you cut at too low speeds or apply any vertical pressure.&lt;/li&gt;&#10;&lt;li&gt;Cut one hole in the cabinet&amp;rsquo;s top panel that you prepared. I used &lt;code&gt;76mm&lt;/code&gt; but nevermind, keep the &lt;code&gt;75mm&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Change the hole diameter on your cutter to 92mm and repeat for the Low/Mid speaker.&lt;/li&gt;&#10;&lt;li&gt;Repeat this cut as the shell&amp;rsquo;s center hole.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/shell_holes.jpg" alt="Image: Holes cut in the shell"&gt;&lt;/figure&gt;&#10;&lt;h3 id="routing"&gt;Routing&lt;/h3&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/router_topbottom.jpg" alt="Image: cutting top/bottom"&gt;&lt;/figure&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Put your shell onto the board you&amp;rsquo;re going to cut top and bottom of the cabinet from. Transfer its inner and outer diameters onto the board.&lt;/li&gt;&#10;&lt;li&gt;Find the center of this circle, e.g. by using the &lt;a href="https://www.wikihow.com/Find-the-Center-of-a-Circle" target="_blank" rel="noopener noreferrer" class="external-link"&gt;method 3 of this guide&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/li&gt;&#10;&lt;li&gt;Mount the compasses tool onto your router, use a &lt;code&gt;12mm&lt;/code&gt; straight cutter head.&lt;/li&gt;&#10;&lt;li&gt;Cut the &amp;ldquo;inner&amp;rdquo; circle for the notch. I&amp;rsquo;d propose a depth of &lt;code&gt;4mm&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;(optional) Mount the &lt;code&gt;8mm&lt;/code&gt; cutter head.&lt;/li&gt;&#10;&lt;li&gt;Cut the &amp;ldquo;outer&amp;rdquo; circle in a couple of steps until you have the full depth of your board cut.&lt;/li&gt;&#10;&lt;li&gt;Turn the circle around, and with e.g. a radius cutter, chamfer the edges.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="carving"&gt;Carving&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/adapterboard_curve.jpg" alt="Image: 3D carving of the adapter plates"&gt;&lt;/figure&gt;&#10;Both the adapter plates and the battery mount sit at the shell&amp;rsquo;s inner/outer diameter, so they need to be curved to be a perfect fit. With hand tools, getting this right is kind of a challenge.&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Measure the cutting depth needed so that when connecting the cuts, a curved surface will be created.&lt;/li&gt;&#10;&lt;li&gt;For the adapter plates, take the inner shell diameter, for the battery mount, use the outer. Note that the speaker adapter plates are convex while the battery holder has to be concave.&lt;/li&gt;&#10;&lt;li&gt;Use your circular saw to make these parallel cuts. Mind the blade&amp;rsquo;s width.&lt;/li&gt;&#10;&lt;li&gt;With a chisel, connect the cuts. Try to work along the grain of the different layers.&lt;/li&gt;&#10;&lt;li&gt;Use a belt sander, your multi tool, or a rasp to smooth out the curvature until it&amp;rsquo;s a good fit.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/adapterboard_done.jpg" alt="Image: sanding the adapter plates"&gt;&lt;/figure&gt;&#10;6. Atach the adapter plates on the inside of the shell using wood glue.&#10;7. Make sure alignment of the holes is good, then take some hot melt to fix and seal the adapters while the glue is setting.&#10;8. After some hours, either smooth out if the overlap is not perfect or take the &lt;code&gt;45°&lt;/code&gt; cutter head and create a chamfer. I found this to be the least controllable bit of the whole build because the router wouldn&amp;rsquo;t sit properly on the curved surface, creating rippled edges. I had to do quite some rework with the multitool.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/shell_chamfered.jpg" alt="Image: shell with adapter plates, chamfered"&gt;&lt;/figure&gt;&#10;&lt;h3 id="additional-wood-works"&gt;Additional wood works&lt;/h3&gt;&#10;&lt;p&gt;Copy the amp carrier Board&amp;rsquo;s dimensions from the &lt;a href="https://blog.schallbert.de/en/anywhereamps-diy/#plans"&gt;plans&lt;/a&gt; section. Use a Forstner drill or the hole cutter to mill the ditch for the potentiometer. A depth of 6mm should be enough to properly mount the potentiometer. For your switch, a countersink might be good to get the diameter just right so that it fits in snugly. Finally, cut the holes for the jacks.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/batterymount.jpg" alt="Image: hand-crafted battery mount"&gt;&lt;/figure&gt;&#10;As there are a lot of battery types around, I didn&amp;rsquo;t create a specific design for the slider and battery contacts. I just used the typical crimp connectors and glued them in the adapter plate so they can establish a good electrical contact to battery plus and minus. As my battery does not have the latch included, I had the slide gradually increase thickness like a wedge so that the battery would &amp;ldquo;stick&amp;rdquo; at the correct place.&lt;/p&gt;&#10;&lt;p&gt;I cut some wooden rods to keep top and bottom of the cabinet closed, and at the same time protecting the wooden surface from bumps. Their length is &lt;code&gt;25mm&lt;/code&gt; more than the total height of the cabinet, and their width is &lt;code&gt;25mm&lt;/code&gt; respective &lt;code&gt;12mm&lt;/code&gt; at the cabinet sides.&lt;/p&gt;&#10;&lt;h3 id="electronics"&gt;Electronics&lt;/h3&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/circuit_board_soldered.jpg" alt="Image: Soldered circuit board"&gt;&lt;/figure&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Cut and drill the circuit board as described in the &lt;a href="https://blog.schallbert.de/en/anywhereamps-diy/#plans"&gt;Plans section&lt;/a&gt;. It is a good idea to print the plan at scale so you can use it as a mask.&lt;/li&gt;&#10;&lt;li&gt;Place the conductor breaks. You can use a &lt;code&gt;3mm&lt;/code&gt; metal drill and hand-cut them if you have no extra tool to do this.&lt;/li&gt;&#10;&lt;li&gt;Craft the jumper wires and place them. Use the &lt;a href="https://blog.schallbert.de/en/anywhereamps-diy/#jumper-wires"&gt;jumper list&lt;/a&gt; as reference.&lt;/li&gt;&#10;&lt;li&gt;Place the resistors.&lt;/li&gt;&#10;&lt;li&gt;Use some tape to fix the parts at the circuit board, flip it, and start soldering.&lt;/li&gt;&#10;&lt;li&gt;Place the capacitors. Mind that you might not have enough space in your housing so that you want to have them mounted horizontally.&lt;/li&gt;&#10;&lt;li&gt;Now place the other components and repeat fixing and soldering them.&lt;/li&gt;&#10;&lt;li&gt;Prepare all connections and wires to the board: Potentiometer, source in, power amp out, &lt;code&gt;VCC/GND&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Check if the power supply section is working. When connecting only &lt;code&gt;VCC/GND&lt;/code&gt;, it should draw between &lt;code&gt;2mA&lt;/code&gt; and &lt;code&gt;10mA&lt;/code&gt; of current depending on potentiometer position and supply voltage. Make sure to avoid reverse polarity, the NE5532 is killed easily (trust me, I know!). You should measure &lt;code&gt;VCC/2&lt;/code&gt; when you probe the virtual ground Op-amp stage&amp;rsquo;s output.&lt;/li&gt;&#10;&lt;li&gt;Check the gain stage. If you have an oscilloscope and a function generator, it should be an easy thing to do. If you don&amp;rsquo;t, connect the input to an audio source, e.g. your smartphone and the output to a &lt;code&gt;2k2&lt;/code&gt; resistor, in series to an LED and a diode like 1N4148 or 1N4007. Turn your smartphone to full volume. The LED should start flickering when turning the resistance of the potentiometer up. At which position flickering starts depends on the LED&amp;rsquo;s forward bias, your supply voltage and your source&amp;rsquo;s maximum output voltage.&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/ampcarrier.jpg" alt="Image: amp carrier and electronics enclosure"&gt;&lt;/figure&gt;&lt;/li&gt;&#10;&lt;li&gt;I propose to mount all the electronics and the audio input - except the &lt;code&gt;9V&lt;/code&gt; supply regulator - in a metal enclosure to reduce electromagnetic interference. Depending on your power amp&amp;rsquo;s wattage, you might need passive or even active radiators. For me, some holes in the enclosure will do fine.&lt;/li&gt;&#10;&lt;li&gt;Mount the enclosure to the amp adapter plate.&lt;/li&gt;&#10;&lt;li&gt;Cut holes in the enclosure as indicated on the adapter plate.&lt;/li&gt;&#10;&lt;li&gt;Insert switch, potentiometer and jacks. Solder the connecting wires.&lt;/li&gt;&#10;&lt;li&gt;Add a fuse, e.g. &lt;code&gt;4.0A&lt;/code&gt; to the setup on the negative path to battery for added safety.&lt;/li&gt;&#10;&lt;li&gt;Mount preamp and power amp and connect them properly.&lt;/li&gt;&#10;&lt;li&gt;Test it! E.g. use an old speaker of yours. Without any source connected, you should hear only so much as a little hissing from the speaker at full gain. When you connect your smartphone, start with gain at &lt;code&gt;0&lt;/code&gt; and low smartphone volume. Then increase the preamp gain to check if the setup works fine.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="putting-it-all-together"&gt;Putting it all together&lt;/h3&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/speakers_mounted.jpg" alt="Image: chassis mounted in the cabinet"&gt;&lt;/figure&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Mount the speakers. Use hot melt or a foam/rubber seal to make the connections airtight&lt;/li&gt;&#10;&lt;li&gt;Use hot melt to close any holes left, e.g. for the screws.&lt;/li&gt;&#10;&lt;li&gt;Solder the tweeters in parallel to the Bass speaker and connect the output cable from your amplifier.&lt;/li&gt;&#10;&lt;li&gt;Mount the battery holder and connect its wires to the amp&amp;rsquo;s terminals.&lt;/li&gt;&#10;&lt;li&gt;Put the seal around top and bottom plate so that no air can escape when mounted.&lt;/li&gt;&#10;&lt;li&gt;Flip the assembly and put the &amp;ldquo;top&amp;rdquo; plate with the amplifier on it in its place like shown on the picture.&lt;/li&gt;&#10;&lt;li&gt;Make sure that none of the speaker terminals touches the amplifier housing, risk of a short-circuiting the power amp.&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/inside_thumb.jpg" alt="Image: all compounents in place"&gt;&lt;/figure&gt;&lt;/li&gt;&#10;&lt;li&gt;The simulation shows that the Bass reproduction will improve if you add damping wool. I slayed an old cushion to get some.&lt;/li&gt;&#10;&lt;li&gt;Close the cabinet and fix it with the wooden stands you prepared earlier. Or use screws instead.&lt;/li&gt;&#10;&lt;li&gt;You&amp;rsquo;re all set (for now)! You earn an applause! Doesn&amp;rsquo;t it look gorgeous? But sure you&amp;rsquo;ll want to give it a try&amp;hellip;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="fine-tuning"&gt;Fine tuning&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Too much treble? Check how much higher the sensitivity ratings of your tweeters are compared to the Bass chassis. If it&amp;rsquo;s not on par, solder 1W resistors in series to the tweeters. I have ratings of &lt;code&gt;96dB@1W/1m&lt;/code&gt; vs &lt;code&gt;80dB@1W/1m&lt;/code&gt; according to the &lt;a href="https://blog.schallbert.de/en/projects/aa_alpha/#sound-hardware"&gt;chassis&amp;rsquo; technical data&lt;/a&gt;. So I&amp;rsquo;d have to get the tweeters a lot quieter. I tried with an &lt;code&gt;1k&lt;/code&gt; potentiometer and finally selected a resistance of &lt;code&gt;470Ohm&lt;/code&gt; which sounded just right.&lt;/li&gt;&#10;&lt;li&gt;The Bass flutters at high volume? Most likely, your cabinet is not airtight. Put on a test tone, e.g. 35HZ &lt;a href="https://www.youtube.com/watch?v=yvHCAne_VLo" 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;, and check if you hear or feel where the air is coming through. Fix the leak(s).&lt;/li&gt;&#10;&lt;li&gt;Your 9V switching buck converter emits noise into your amplifier setup? Try smoothing it with a &lt;code&gt;100nF&lt;/code&gt; capacitor, and maybe add a &lt;code&gt;10k&lt;/code&gt; parallel resistor to create a dummy load. If this doesn&amp;rsquo;t help, take a linear converter instead like &lt;a href="https://www.elektronik-kompendium.de/public/schaerer/ureg3pin.htm" target="_blank" rel="noopener noreferrer" class="external-link"&gt;LM7809&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. As it dissipates much more heat than its switching colleague, plan for mounting it to your metal enclosure.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;&lt;a href="https://blog.schallbert.de/en/projects/aa_alpha/"&gt;Back to Project main page&lt;/a&gt;&lt;/p&gt;&#10;</description></item><item><title>CNC job setup</title><link>https://blog.schallbert.de/en/cnc-job-setup/</link><pubDate>Mon, 22 Aug 2022</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/cnc-job-setup/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-08-22_cnc_job_setup-thumb.jpg"&#10; class="post-cover"&#10; alt="Image of a CNC job demo"&#10; title="CNC job setup" /&gt;&#10;&lt;h2 id="demo-video-about-cnc-job-preparation--execution"&gt;Demo video about CNC job preparation &amp;amp; execution&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;iframe&#10; src="https://makertube.net/videos/embed/mFreMUKxXktZH3kpqdENGJ"&#10; title="How-to: CNC job setup &amp;amp;amp; quick demo"&#10; loading="lazy"&#10; allow="autoplay; fullscreen; picture-in-picture"&#10; allowfullscreen&#10; style="width: 100%; height: auto; min-height: 315px; aspect-ratio: 16/9;"&#10; &gt;&lt;/iframe&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;How-to: CNC job setup &amp;amp;amp; quick demo&lt;/span&gt;&lt;a href="https://makertube.net/videos/embed/mFreMUKxXktZH3kpqdENGJ" class="attr-link" target="_blank" rel="noopener noreferrer"&gt;↗&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;The following sections summarize preparation steps from the video.&lt;/p&gt;&#10;&lt;h3 id="job-prepare-checklist"&gt;Job prepare checklist&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Securely mount your workpiece on the CNC&lt;/li&gt;&#10;&lt;li&gt;Transfer the job data to your CNC&amp;rsquo;s computer&lt;/li&gt;&#10;&lt;li&gt;Make sure Emergency Stop is engaged&lt;/li&gt;&#10;&lt;li&gt;Power up the CNC&lt;/li&gt;&#10;&lt;li&gt;Connect your computer to the machine and start the CNC software&lt;/li&gt;&#10;&lt;li&gt;Disengage the Emergency Stop&lt;/li&gt;&#10;&lt;li&gt;Prepare and check required tools for damages&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="cnc-initialize-checklist"&gt;CNC initialize checklist&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Acknowledge the Emergency Stop release&lt;/li&gt;&#10;&lt;li&gt;Activate stepper drivers&lt;/li&gt;&#10;&lt;li&gt;Reference the machine&lt;/li&gt;&#10;&lt;li&gt;Change to the first tool you&amp;rsquo;ll be using&lt;/li&gt;&#10;&lt;li&gt;Set X, Y zero&lt;/li&gt;&#10;&lt;li&gt;Measure Z height / set Z zero&lt;/li&gt;&#10;&lt;li&gt;Put CNC back to home position&lt;/li&gt;&#10;&lt;li&gt;Perform spindle warmup run&lt;/li&gt;&#10;&lt;li&gt;Check Auxiliary devices (e.g. cooling, dust collection)&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="during-the-job"&gt;During the job&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Supervise the machine&lt;/li&gt;&#10;&lt;li&gt;If needed, perform tool changes&lt;/li&gt;&#10;&lt;li&gt;If required, pause the job when something doesn&amp;rsquo;t seem right&lt;/li&gt;&#10;&lt;li&gt;Have the emergency stop in reach for worst case scenarios&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="job-completed"&gt;Job completed&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Set the machine back to home position&lt;/li&gt;&#10;&lt;li&gt;Engage emergency stop and disconnect the computer&lt;/li&gt;&#10;&lt;li&gt;Disable power supply to the machine&lt;/li&gt;&#10;&lt;li&gt;Take your workpiece from the machine bed&lt;/li&gt;&#10;&lt;li&gt;Cleanup&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item><item><title>Measure Inrush current</title><link>https://blog.schallbert.de/en/measure-inrush-current/</link><pubDate>Tue, 16 Aug 2022</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/measure-inrush-current/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-08-16_inrush-thumb.jpg"&#10; class="post-cover"&#10; alt="Inrush current measurement"&#10; title="Measure Inrush current" /&gt;&#10;&lt;h2 id="setting-up-the-multimeter--oscilloscope--data-logger"&gt;Setting up the multimeter / oscilloscope / data logger&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-08-16_multimeter.jpg" alt="Image: Metrahit Energy"&gt;&lt;/figure&gt;&#10;&lt;p&gt;For this blog post, I was lucky to borrow a multimeter for energy measurement from &lt;a href="https://www.gossenmetrawatt.de/en/products/measuring-and-test-technology/multimeters-and-handheld-calibrators/special-applications/multimeters-for-power-and-energy-measurement/metrahit-energy/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;GMC Instruments&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. It features a data logger so I can visualize the collected data.&lt;/p&gt;&#10;&lt;h3 id="metrahit-energy"&gt;Metrahit Energy&lt;/h3&gt;&#10;&lt;p&gt;I&amp;rsquo;d rate the &amp;ldquo;Metrahit Engergy&amp;rdquo; to be more in the &amp;lsquo;pro&amp;rsquo; than in the &amp;lsquo;hobbyist&amp;rsquo; corner of multimeters, both its wealth of features and its price tag point in that direction. In contrast to lower-range models, it provides modes for instantaneous and average voltage/current, is able to measure power, power quality, and energy consumption of devices. It is also able to show total harmonic distorion of a signal and to read PT100 industry-grade temperature sensors.&lt;/p&gt;&#10;&lt;p&gt;This piece of equipment might come in handy for future articles as well.&lt;/p&gt;&#10;&lt;h3 id="metrawin-software-setup"&gt;MetraWin software setup&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Download and install the &lt;a href="https://www.gmc-instruments.de/en/products/metrawin-10" target="_blank" rel="noopener noreferrer" class="external-link"&gt;MetraWin&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; software to transfer and display collected data from the multimeter&lt;/li&gt;&#10;&lt;li&gt;Download and install &lt;a href="https://www.gmc-instruments.de/en/products/software-and-accessories/software/product-specific-software/test-technology/driver-control/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Driver-Control&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; software. It&amp;rsquo;s purpose is to provide a communications port between USB and the infrared communication interface adapter provided with the multimeter.&lt;/li&gt;&#10;&lt;li&gt;In MetraWin software, select &lt;code&gt;METRAHit Starline Series&lt;/code&gt; (btw, unclear to me why the series&amp;rsquo; name neither appears on the device nor the website)&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-08-16_metrawin_device.jpg" alt="Image of MetraWin settings to choose device"&gt;&lt;/figure&gt;&lt;/li&gt;&#10;&lt;li&gt;Connect the infrared adapter. In the device manager (&lt;code&gt;Windows-key&lt;/code&gt; &amp;ndash;&amp;gt; type &lt;code&gt;device&lt;/code&gt; and select &lt;code&gt;device manager&lt;/code&gt; &amp;ndash;&amp;gt; scroll down to &lt;code&gt;Connections (COM &amp;amp; LPT)&lt;/code&gt;, unfold), make sure the IR adapter shows up with a &lt;code&gt;COM&lt;/code&gt; port ID). Select this ID in MetraWin&amp;rsquo;s&lt;code&gt; Device --&amp;gt; Communication... --&amp;gt; SEL-COM&lt;/code&gt; window.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="multimeter-setup-for-inrush-current-logging"&gt;Multimeter setup for inrush current logging&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-08-16_addr.jpg" alt="Image: Multimeter address setting"&gt;&lt;/figure&gt; In the Multimeter&amp;rsquo;s settings, make sure the address is set to any other value than &lt;code&gt;00&lt;/code&gt; (&lt;code&gt;Setup --&amp;gt; Set --&amp;gt; System --&amp;gt; Addr&lt;/code&gt;) because the infrared communication will not detect the device with an invalid value here (I experienced this and needed quite some time to figure out that not the IR adapter detection is faulty but the Multimeter&amp;rsquo;s settings). Background is that by address, the software supports up to 15 devices simultaneously.&lt;/li&gt;&#10;&lt;li&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-08-16_rate.jpg" alt="Image: Multimeter sample time"&gt;&lt;/figure&gt; Set the sample time to its lowest value for good impulse tracking: go to &lt;code&gt;Measure --&amp;gt; Store --&amp;gt; Rate&lt;/code&gt; and select &lt;code&gt;0.0005sec&lt;/code&gt; which means the logger takes measurements with &lt;code&gt;2000Hz&lt;/code&gt; sample rate. This is as low as it gets, and these values already compromise the recording channel availability e.g. for energy measurement due to the device&amp;rsquo;s limited buffering ability.&lt;/li&gt;&#10;&lt;li&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-08-16_trms.jpg" alt="Image: Multimeter TRMS setting"&gt;&lt;/figure&gt; Now select the value you want to measure (for me it is &lt;code&gt;DC&lt;/code&gt; current in &lt;code&gt;Ampère&lt;/code&gt;), hit &lt;code&gt;MAN/AUTO&lt;/code&gt; and choose the range manually to a value higher than the peak value you&amp;rsquo;re expecting. Hint: If you just select &lt;code&gt;A&lt;/code&gt; and the Multimeter shows &lt;code&gt;TRMS&lt;/code&gt; or &lt;code&gt;RMS&lt;/code&gt; somewhere, this is likely not what you want because with this option, the device will do some maths to get average values which naturally does not work when we want signal peaks.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;For my amplifier&amp;rsquo;s inrush current, I had to set the range manually to &lt;code&gt;&amp;gt;10A&lt;/code&gt;. This is because if the signal peak exceeds the &amp;lsquo;Auto&amp;rsquo; range by far, accuracy will be low and the device might even record complete nonsense.&lt;/p&gt;&#10;&lt;h3 id="alternative-devices"&gt;Alternative devices&lt;/h3&gt;&#10;&lt;p&gt;Oscilloscopes would easily outperform the multimeter in terms of sample rate but as they have completely different design requirements, it&amp;rsquo;s fair to say that you cannot really compare these two device categories.&lt;/p&gt;&#10;&lt;p&gt;Nevermind, you can do the exact same measurement with the cheapest of scopes e.g. connected to a &lt;code&gt;0.1 Ohm&lt;/code&gt; shunt resistor as well, and set it to &lt;code&gt;1V/div&lt;/code&gt;. What for this specific task likely won&amp;rsquo;t work is a low-cost multimeter with &amp;ldquo;Data Max Hold&amp;rdquo; function. For capacitor charging on a DC power supply, the sample rate will be just too low.&lt;/p&gt;&#10;&lt;h2 id="discussion-of-circuit-to-measure"&gt;Discussion of circuit to measure&lt;/h2&gt;&#10;&lt;p&gt;The device under test is a &lt;code&gt;30W&lt;/code&gt; power amplifier board &lt;a href="https://store.sure-electronics.com/product/AA-AB32996" target="_blank" rel="noopener noreferrer" class="external-link"&gt;AA-AB32996&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; by sure electronics based on the Class-D integrated circuit &lt;a href="https://www.ti.com/product/TPA3130D2" target="_blank" rel="noopener noreferrer" class="external-link"&gt;TPA3110&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-08-16_TPA3110.jpg" alt="Image: sure electronics AA-AB32996 based on TPA3110"&gt;&lt;/figure&gt;&#10;&lt;p&gt;The supply path involves reverse voltage protection schottky diodes (three in parallel), connected to 6 bulk &lt;code&gt;220uF&lt;/code&gt; electrolytic capacitors on the positive supply rails of the amplifier IC. In addition, two different decoupling capacitors close to the IC reduce noise on that rail.&lt;/p&gt;&#10;&lt;p&gt;For comparison, the PCB layout example looks similar. You can see that the example features additional &lt;code&gt;FB&lt;/code&gt; (ferrite beads) components for current leveling on the input rail and has less bulk capacitance for current surge handling capabilities. Also, the example works with more simple ferrite beads instead of &amp;ldquo;proper&amp;rdquo; inductor filters on the speaker output.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-08-16_TPA3110_recommended_layout.jpg" alt="Image: TPA3110 recommended PCB layout"&gt;&lt;/figure&gt;&#10;&lt;h3 id="motivation-failed-prototyping-with-usb-c-pd"&gt;Motivation: Failed Prototyping with USB-C PD&lt;/h3&gt;&#10;&lt;p&gt;For one of my prototypes, I connected the USB-C Power Delivery development board &lt;a href="https://blog.schallbert.de/en/usbpd-explained/#stusb4500"&gt;I discussed in this post&lt;/a&gt; and had to learn that it would blow up after just a few switching cycles - supposedly due to a high inrush current the power delivery device couldn&amp;rsquo;t handle.&lt;/p&gt;&#10;&lt;p&gt;That&amp;rsquo;s why I wanted to know how high the inrush current actually is and with which component I could effectively reduce the risk of voltage instability or even component damage when switching the amplifier on.&lt;/p&gt;&#10;&lt;h2 id="actual-measurement"&gt;Actual measurement&lt;/h2&gt;&#10;&lt;p&gt;I connected the multimeter&amp;rsquo;s current input to the negative power supply path of the amplifier, its GND connector to the power supply&amp;rsquo;s &lt;code&gt;GND&lt;/code&gt;, &lt;code&gt;+20V&lt;/code&gt; of the power supply to the amp&amp;rsquo;s positive power in, and the Multimeter&amp;rsquo;s voltage input to the IC&amp;rsquo;s power supply pins.&lt;/p&gt;&#10;&lt;p&gt;Then I selected &lt;code&gt;SET --&amp;gt; Store --&amp;gt; Start --&amp;gt; Store&lt;/code&gt; to begin the measurement and connected the positive supply path. As voltage/current stabilized, I ended the measurement by pressing &lt;code&gt;Stop&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Due to the high data rate, I had to transfer the data to my computer manually and couldn&amp;rsquo;t use the &amp;ldquo;live&amp;rdquo; function. To do this, I hit &lt;code&gt;Read Memory --&amp;gt; Get Memory Info --&amp;gt; Move Memory Data to File --&amp;gt; save --&amp;gt; Open Memory Data File --&amp;gt; Display Data from File&lt;/code&gt;. Not entirely clear to me why the process has to be that complex - I&amp;rsquo;d like a shortcut to directly display data from the device - but nevermind, it works pretty well this way.&lt;/p&gt;&#10;&lt;p&gt;The &lt;code&gt;Measurement Data File&lt;/code&gt; has a significant advantage over the &lt;code&gt;Memory Data File&lt;/code&gt;: It contains metadata like scoped view, labels, units per division etc. When complete, a PDF can be generated with no big hassle.&lt;/p&gt;&#10;&lt;h3 id="inrush-current-event"&gt;Inrush current event&lt;/h3&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-08-16_TPA3110_inrushcurrent.jpg" alt="Image: TPA3110 current over time"&gt;&lt;/figure&gt;&#10;&lt;p&gt;The whole event only took &lt;code&gt;~10ms&lt;/code&gt;, 20 samples. After a really high current spike of &lt;code&gt;&amp;gt;12A&lt;/code&gt;, there&amp;rsquo;s a current plateau at around &lt;code&gt;4A&lt;/code&gt; until the capacitors are charged and the reduces to the circuit&amp;rsquo;s normal idle level.&lt;/p&gt;&#10;&lt;p&gt;The peak current has 1 sample only - I cannot say whether this is an outlier/ measurement error or a real event. Some assumptions for its cause:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;very low ESR small bypass within circuits&lt;/li&gt;&#10;&lt;li&gt;Sample rate still too low&lt;/li&gt;&#10;&lt;li&gt;Current meter range violation&lt;/li&gt;&#10;&lt;li&gt;Power supply current limit engaged creating the current plateau (set to &lt;code&gt;5A&lt;/code&gt; so it does not really fit)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="voltage-amplifiers-supply-pins"&gt;Voltage @Amplifier&amp;rsquo;s supply pins&lt;/h3&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-08-16_TPA3110_inrushvoltage.jpg" alt="Image: TPA3110 voltage over time"&gt;&lt;/figure&gt;&#10;&lt;p&gt;The voltage measurement shows a steep incline for the first sample when the inrush event starts, followed by an almost linear increase until steady state voltage is reached after &lt;code&gt;~6ms&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;These values correspond nicely to the current peak at the very beginning and the plateau showing the capacitors gradually charge until target voltage is reached.&lt;/p&gt;&#10;&lt;h3 id="repetition--conclusion"&gt;Repetition &amp;amp; Conclusion&lt;/h3&gt;&#10;&lt;p&gt;I repeated the measurement with different settings: Speakers connected/disconnected, amplifier in standby/idle but the results were identical. This led me to the conclusion that the amplifier IC and its periphery do not affect inrush current - it only depends on the power supply path and its protection, decoupling, and energy buffering function.&lt;/p&gt;&#10;&lt;p&gt;Drawbacks of doing this measurement with a multimeter are a limited data rate (I would have liked to measure with 10&amp;hellip;100kHz for these surge events), the inability to live-view, and long data transfer times due to a (probably outdated) infrared communication standard. On the other hand, I am now able to do &amp;ldquo;offline logs&amp;rdquo; very quickly and without the need of a computer or even mains connection. Setup is easy and device portability is great for field application - I&amp;rsquo;ll do some more measurements in the future where the multimeter might be an even better fit.&lt;/p&gt;&#10;&lt;p&gt;For the amplifier, I will design a protective circuit as already discussed &lt;a href="https://blog.schallbert.de/en/inrushlimiter-circuits/"&gt;in this post&lt;/a&gt; to mitigate issues with USB-PD devices.&lt;/p&gt;&#10;</description></item><item><title>My CNC vibrates</title><link>https://blog.schallbert.de/en/cnc-vibrates/</link><pubDate>Thu, 14 Jul 2022</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/cnc-vibrates/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-07-14_vibrations-thumb.jpg"&#10; class="post-cover"&#10; alt="Distorted image of a circle cut with my CNC"&#10; title="My CNC vibrates" /&gt;&#10;&lt;h2 id="background"&gt;Background&lt;/h2&gt;&#10;&lt;p&gt;Lately, I have been pushing my machine closer to its limits. I&amp;rsquo;m now often formatting plywood full-slot with one pass meaning the machine has to take a &lt;code&gt;Z+&lt;/code&gt; of &lt;code&gt;12mm&lt;/code&gt; at &lt;code&gt;F2700mm/min&lt;/code&gt; on a 6mm 2-flute rougher bit at &lt;code&gt;S=24000rpm&lt;/code&gt;. I&amp;rsquo;m using climb milling for my projects in wood.&lt;/p&gt;&#10;&lt;p&gt;Now the machine sometimes starts vibrating.&lt;/p&gt;&#10;&lt;p&gt;You might say &amp;ldquo;just reduce machine load by setting a more shallow depth of cut&amp;rdquo;, &amp;ldquo;slow down your feed rate and spindle RPM&amp;rdquo;, or &amp;ldquo;Don&amp;rsquo;t take a rougher, that&amp;rsquo;s always inducing more vibration&amp;rdquo; - and I might follow of your advice at some point, but first I want to understand what&amp;rsquo;s exactly going wrong here and whether I can fix this issue without sacrificing machine time or cutter life.&lt;/p&gt;&#10;&lt;h2 id="problem-description"&gt;Problem description&lt;/h2&gt;&#10;&lt;p&gt;Machine vibration doesn&amp;rsquo;t only happen while formatting but also when I cut pockets, but it seems like only at certain positions during the cut. It feels like the whole machine is shaking just a little, but the sound of it makes me doubt that everything is alright. I captured two samples for your reference below.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2022-07-14-machine_vibrations_pocketing.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Vibrations manifest at certain positions of the machine while pocketing (80% clearing feed)&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2022-07-14-machine_vibrations_slot.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Vibrations during formatting&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="analysis"&gt;Analysis&lt;/h2&gt;&#10;&lt;p&gt;Cutting results are looking good to me still&amp;hellip;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-07-14_cut_results.jpg" alt="Despite vibrating, the cut still looks clean"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;p&gt;I retried with somewhat different settings: Reduced &lt;code&gt;Z+&lt;/code&gt; of only &lt;code&gt;6mm&lt;/code&gt; but increased feed of &lt;code&gt;3600mm/min&lt;/code&gt; with the same bit I used before - vibrations gone.&lt;/p&gt;&#10;&lt;p&gt;After contacting the machine manufacturer &lt;a href="https://hobbyline.info/forum/index.php?thread/686-meine-maschine-vibriert-erledigt/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;(refer to this thread in German)&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, the most likely reason for the vibration is the router and its clamp that mounts to the Z-axis.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-07-14_routermount.jpg" alt="Router mounting to the Z-axis"&gt;&lt;/figure&gt;&#10;The router has less high-end bearings than heavy-duty spindles so they are subject to play. In addition, the 42mm standard clamp is attached to the Z-axis with two M8 fasteners while the router is clamped with only one fastener around its &amp;ldquo;neck&amp;rdquo;.&lt;/p&gt;&#10;&lt;p&gt;HF-spindles are most often flush-fastened directly to the Z-axis so the contact surface is incomparably higher, leaving little or no room for vibration.&lt;/p&gt;&#10;&lt;h2 id="result"&gt;Result&lt;/h2&gt;&#10;&lt;p&gt;I have three options now:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Continue with high depth milling and tolerate vibrations and additional noise&lt;/li&gt;&#10;&lt;li&gt;Change to more shallow passes which solves vibration issues but adds machining time&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/projects/spindle-upgrade/"&gt;Upgrade the spindle&lt;/a&gt; which adds considerable cost to my setup&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/why-vacuum-table/"&gt;Upgrade the machine bed&lt;/a&gt; to stiffen the machine frame&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Update: Machine vibrations&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2023-09-12T00:00:00Z"&gt;&#10; 2023-09-12&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; In the meantime, I upgraded both &lt;a href="https://blog.schallbert.de/en/why-vacuum-table/"&gt;machine bed&lt;/a&gt; and &lt;a href="https://blog.schallbert.de/en/projects/spindle-upgrade/"&gt;spindle&lt;/a&gt;. The vibrations are still there. Frustrating. When I look at it again, I see vibrations emerge when moving both &lt;code&gt;X&lt;/code&gt; and &lt;code&gt;Y&lt;/code&gt; axis. When I move &lt;code&gt;Y&lt;/code&gt; only, vibrations are gone. In case I move &lt;code&gt;X&lt;/code&gt;, though, there&amp;rsquo;s a small amount of vibrations. Working assumption is that the root cause lies in axis stability. I have quite a wide machine with just one ball screw in its center, and one instead of two linear guides on the Z-axis. The situation improves when I center the workpiece in X-direction, so that there&amp;rsquo;s no imbalance left to right when the portal moves.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;</description></item><item><title>Image to Path</title><link>https://blog.schallbert.de/en/path-from-image/</link><pubDate>Mon, 04 Jul 2022</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/path-from-image/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-07-04_cam-thumb.jpg"&#10; class="post-cover"&#10; alt="IMAGE - CAM drawing of Image-to-Path engraving"&#10; title="Image to Path" /&gt;&#10;&lt;p&gt;This is a short blog post about how to use the two Open-source tools &lt;a href="https://www.getpaint.net/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Paint.NET&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and &lt;a href="https://inkscape.org/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Inkscape&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; to yield paths that can be used e.g. for engravings on a CNC machine, a laser cutter, or a plotter, even from a low-quality digital image in a lossy data compression format like JPEG.&lt;/p&gt;&#10;&lt;p&gt;It was inspired by &lt;a href="https://graphicdesign.stackexchange.com/questions/113560/convert-parallel-paths-into-one-in-inkscape" target="_blank" rel="noopener noreferrer" class="external-link"&gt;this excellent Stackexchange thread&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="get-the-source-image"&gt;Get the source image&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-07-04_original.jpg" alt="IMAGE Campfire - wonderfully drawn by Schallbert"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Obtain the image you want to be working with on your machine.&lt;/p&gt;&#10;&lt;h3 id="intellectual-property"&gt;Intellectual property&lt;/h3&gt;&#10;&lt;p&gt;Note that images (even the ones you did draw yourself) are usually copyright protected so you must first get (written) approval from the owner of the intellectual property.&lt;/p&gt;&#10;&lt;p&gt;If the intellectual rights owner is you, that&amp;rsquo;s much more easy. This is why I chose an image I drew myself for this quick demo.&lt;/p&gt;&#10;&lt;h3 id="image-quality"&gt;Image quality&lt;/h3&gt;&#10;&lt;p&gt;Look at how ugly it is. It really shows I&amp;rsquo;m not an artist. Some lines are drawn multiple times and the contrast is low. Perfect conditions to show what the software can do!&lt;/p&gt;&#10;&lt;p&gt;But remember: Pretty much as always, the credo is &amp;ldquo;Garbage in, garbage out&amp;rdquo;. So you should make sure you get the best possible image quality to start with. Cleaning the image will be much easier this way and your engraving paths will both be simpler and more accurate.&lt;/p&gt;&#10;&lt;h2 id="paintnet"&gt;Paint.NET&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-07-04_luminance.jpg" alt="IMAGE Luminance change for maximum contrast"&gt;&lt;/figure&gt;&#10;&lt;h3 id="groundworks"&gt;Groundworks&lt;/h3&gt;&#10;&lt;p&gt;First, crop your image to the content you want to use later. Then take the &lt;code&gt;luminance curve&lt;/code&gt; tool &lt;code&gt;Adjustments -&amp;gt; Curves&lt;/code&gt; and try to separate highlights and dark tones as much as possible.&lt;/p&gt;&#10;&lt;p&gt;Second, if your image is as bad as mine, use the &lt;code&gt;wand&lt;/code&gt; tool - play around with the tolerances - to knock out the background as well as possible.&lt;/p&gt;&#10;&lt;p&gt;Third, use the &lt;code&gt;outline&lt;/code&gt; tool (&lt;code&gt;Effects -&amp;gt; Stylize -&amp;gt; Outline&lt;/code&gt;) to get a nice and thick line from your image. Desaturating the image could help along with another contrast increase to make this line black. In case you start &amp;ldquo;losing&amp;rdquo; lines that are not dark enough - don&amp;rsquo;t worry, that&amp;rsquo;s easy to fix later on in Inkscape.&lt;/p&gt;&#10;&lt;h3 id="cleanup"&gt;Cleanup&lt;/h3&gt;&#10;&lt;p&gt;Remove as many &amp;ldquo;pixel clouds&amp;rdquo; and other artifacts from the image as possible using &lt;code&gt;wand&lt;/code&gt; or &lt;code&gt;eraser&lt;/code&gt; tools, it&amp;rsquo;s easier to do now than later.&lt;/p&gt;&#10;&lt;p&gt;Save your image as portable network graphic &lt;code&gt;.PNG&lt;/code&gt;&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-07-04_outline.jpg" alt="IMAGE Outline tool for continuous and thick lines"&gt;&lt;/figure&gt;&#10;&lt;h2 id="inkscape"&gt;Inkscape&lt;/h2&gt;&#10;&lt;p&gt;Open the image in inkscape. Chose standard import settings &lt;code&gt;DPI from file&lt;/code&gt; etc.&lt;/p&gt;&#10;&lt;h3 id="tracing"&gt;Tracing&lt;/h3&gt;&#10;&lt;p&gt;Select &lt;code&gt;Path -&amp;gt; Trace Bitmap&lt;/code&gt;. In the new pane, select &lt;code&gt;Trace Bitmap&lt;/code&gt;, &lt;code&gt;Single Scan&lt;/code&gt; &lt;code&gt;Brightness Cutoff&lt;/code&gt;, and play around with the options. Make sure you select the image via mouse click before clicking &lt;code&gt;update&lt;/code&gt; or &lt;code&gt;apply&lt;/code&gt;. If you&amp;rsquo;re not happy with the preview, adjust the cutoff values. In case you&amp;rsquo;re getting lost given the multitude of settings, just have a look at &lt;a href="https://inkscape.org/doc/tutorials/tracing/tutorial-tracing.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;this tutorial by Inkscape&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-07-04_paths.jpg" alt="IMAGE Image to Path tool at its best"&gt;&lt;/figure&gt;&#10;&lt;p&gt;OK, now you have a lot of paths. Click &lt;code&gt;Path -&amp;gt; Simplify&lt;/code&gt; to reduce the number of supporting points. Then continue manually removing artifacts that sneaked back in via the Brightness cutoff with the &lt;code&gt;Edit Paths by Nodes&lt;/code&gt; tool.&lt;/p&gt;&#10;&lt;h3 id="fill-and-stroke"&gt;Fill and Stroke&lt;/h3&gt;&#10;&lt;p&gt;What we have now are two paths outlining the drawing on the outer and the inner, and a fill in between those paths that represents line strengh of the original drawing. As we want to keep the outer stroke / paths only, we&amp;rsquo;ll have to get rid of fill and inner line.&lt;/p&gt;&#10;&lt;p&gt;Select the &lt;code&gt;Stroke and Fill&lt;/code&gt; tool in the lower part of the tool bar on the right. Select &lt;code&gt;Stroke paint&lt;/code&gt; tab and set the stroke to &lt;code&gt;Flat color&lt;/code&gt;. Under &lt;code&gt;Stroke style&lt;/code&gt;, select a line width you&amp;rsquo;re comfortable with. Then go to &lt;code&gt;Fill&lt;/code&gt; tab and set it to &lt;code&gt;No paint&lt;/code&gt;. Now you&amp;rsquo;re only left with two outline paths of which you&amp;rsquo;re free to delete one if you wish.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-07-04_paths_cleaned.jpg" alt="IMAGE Paths cleaned up and simplified"&gt;&lt;/figure&gt;&#10;&lt;h3 id="further-cleanup"&gt;Further cleanup&lt;/h3&gt;&#10;&lt;p&gt;This looks pretty good already, doesn&amp;rsquo;t it? If you don&amp;rsquo;t mind having outlines more than a single stroke image, you&amp;rsquo;re done already. You could now further improve the image by pulling out paths or transforming curves. Or, if you want just a few lines to be cut, continue.&lt;/p&gt;&#10;&lt;p&gt;Use the &lt;code&gt;break path at selected nodes&lt;/code&gt; tool before removing parallel lines so you don&amp;rsquo;t accidentally kill the lines you&amp;rsquo;d like to keep. Once you&amp;rsquo;re happy with the clean up and every wrinkle is ironed out, save your work as scalable vector graphic file &lt;code&gt;.SVG&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;That&amp;rsquo;s it! Now your CAM or any other machine toolchain you&amp;rsquo;re using can import the image (remember the DPI setting so your scaling remains valid).&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-07-04_clean.jpg" alt="IMAGE Fireplace SVG path work complete"&gt;&lt;/figure&gt;&#10;</description></item><item><title>CNC router overload</title><link>https://blog.schallbert.de/en/cnc-router-overload/</link><pubDate>Sat, 18 Jun 2022</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/cnc-router-overload/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-06-18_overload-thumb.jpg"&#10; class="post-cover"&#10; alt="Image of an endmill path that doesn&amp;#39;t look good at all"&#10; title="CNC router overload" /&gt;&#10;&lt;h2 id="what-i-wanted-to-do"&gt;What I wanted to do&lt;/h2&gt;&#10;&lt;p&gt;I had a request to cut and engrave high pressure laminates. These are a lasagna of compressed paper and &lt;a href="https://en.wikipedia.org/wiki/Phenol_formaldehyde_resin" target="_blank" rel="noopener noreferrer" class="external-link"&gt;phenolic resin&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, coated in a thin layer of (colored) &lt;a href="https://en.wikipedia.org/wiki/Melamine_resin" target="_blank" rel="noopener noreferrer" class="external-link"&gt;melamine resin&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. HPL is really so durable that it&amp;rsquo;s even taken for facade planking. It is dishwasher safe and some variants can be heated to 200°C without taking any damage. You can find HPL as boards on playgrounds, garden tables, as panels in the tram etc.&#10;So I was like &amp;ldquo;cool stuff, let&amp;rsquo;s get started!&amp;rdquo;&lt;/p&gt;&#10;&lt;h2 id="cutting-data-in-high-pressure-laminates-hpl-resopal"&gt;Cutting data in High Pressure Laminates (HPL, &amp;ldquo;Resopal&amp;rdquo;)&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-06-18_overload_chips.jpg" alt="HPL and chips after cutting"&gt;&lt;/figure&gt;&#10;I used the cutting data recommended by a couple of websites with values for &amp;ldquo;hard wood&amp;rdquo;.&#10;2-flute upcut wood carbide endmill, speed of cut &lt;code&gt;450m/min&lt;/code&gt;, &lt;code&gt;6mm&lt;/code&gt;, &lt;code&gt;Z+ 6,5mm&lt;/code&gt;, &lt;code&gt;S24000rpm&lt;/code&gt;, &lt;code&gt;F3500mm/min&lt;/code&gt;, full slot, climb milling.&lt;/p&gt;&#10;&lt;h2 id="cutting-results"&gt;Cutting results&lt;/h2&gt;&#10;&lt;p&gt;And I was alarmed by the way the cut looked when I was done.&#10;Here are the symptoms:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;While cutting, spindle RPM would drop repeatedly but catch up to desired speed again quickly.&lt;/li&gt;&#10;&lt;li&gt;high chipload on the bit, big chips, looking all fine&lt;/li&gt;&#10;&lt;li&gt;endmill didn&amp;rsquo;t follow a straight line: it somehow &amp;ldquo;wobbled&amp;rdquo;.&lt;/li&gt;&#10;&lt;li&gt;when cutting taps to keep the material in place, the cutter would move sideways a bit although it shouldn&amp;rsquo;t&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="searching-the-culprit"&gt;Searching the culprit&lt;/h2&gt;&#10;&lt;p&gt;First, I checked the spindle. No overheating, still tightly clamped to the portal milling machine, no bearing clearance, endmill securely fastened. Then I thought &amp;ldquo;Maybe something is wrong with the portal? Can I bend it?&amp;rdquo; No, I couldn&amp;rsquo;t. Then I looked at the endmill.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-06-18_overload_endmill.jpg" alt="dull carbide endmill after cutting HPL"&gt;&lt;/figure&gt;&#10;It didn&amp;rsquo;t look good at all. The cutting edges were practically gone where they had touched the material. Strange. It was brand-new when I started the job and it didn&amp;rsquo;t even run for 20 minutes.&lt;/p&gt;&#10;&lt;h2 id="analysis-results"&gt;Analysis results&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;I used much too aggressive cutting data&lt;/li&gt;&#10;&lt;li&gt;Because of that, the router motor was operated way outside its capabilities&lt;/li&gt;&#10;&lt;li&gt;This caused sudden drops of the motor&amp;rsquo;s rotational speed&lt;/li&gt;&#10;&lt;li&gt;This again led to increased chipload of my endmill&lt;/li&gt;&#10;&lt;li&gt;As bearings and attachment of the router to the portal are &amp;ldquo;soft&amp;rdquo;, the cutter tried to evade from its cutting path&lt;/li&gt;&#10;&lt;li&gt;This probably saved me from having a shattered endmill though&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="improvements"&gt;Improvements&lt;/h2&gt;&#10;&lt;h3 id="researching-hpl-parameters-again"&gt;Researching HPL parameters (again)&lt;/h3&gt;&#10;&lt;p&gt;I did some more research on the internet. HPL is incredibly hard and tough. To compare some values (Brinell-hardness):&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Beech: &lt;code&gt;~3.7N/mm²&lt;/code&gt; (&lt;a href="https://www.olivenholzprodukte.de/Holzhaerte-Tabelle-nach-Brinell" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Source&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;)&lt;/li&gt;&#10;&lt;li&gt;HPL: &lt;code&gt;~185N/mm²&lt;/code&gt; (&lt;a href="https://www.pro-hpl.org/assets/uploads/prohpl/files/200330_Technische_Kennziffern_und_physikalische_Eigenschaften_von_HPL.pdf" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Source&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;)&lt;/li&gt;&#10;&lt;li&gt;Mild steel ST70: &lt;code&gt;215N/mm²&lt;/code&gt; (&lt;a href="https://www.ingenieurkurse.de/baustofftechnik-1/stoffeigenschaften-im-bauwesen/mechanische-eigenschaften-von-baustoffen/oberflaechenhaerte/brinellhaerte.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Source&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;p&gt;I have found some recommendations for cutting HPL like &amp;ldquo;Carbide endmills should have a cutting speed of up to &lt;a href="https://www.dekorplatten.de/sites/default/files/dateien/HPL-Verarbeitungshinweise.pdf" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;code&gt;150m/min&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&amp;rdquo; and &amp;ldquo;Use diamond-hardened cutters or 1-flute ones that are optimized for Aluminium&amp;rdquo;.&lt;/p&gt;&#10;&lt;h3 id="interim-containment"&gt;Interim containment&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-06-18_overload_corrected.jpg" alt="dull carbide endmill after cutting HPL"&gt;&lt;/figure&gt;&#10;To correct things, I just did all cutouts again at finishing settings in CAM. With a fresh endmill and less aggressive feed rates of &lt;code&gt;F2500mm/min&lt;/code&gt; and an updated depth per pass of &lt;code&gt;Z+1,5mm&lt;/code&gt;.&#10;The result looks much better now.&lt;/p&gt;&#10;&lt;h3 id="optimized-cutting-data"&gt;Optimized cutting data&lt;/h3&gt;&#10;&lt;p&gt;Still, let&amp;rsquo;s recalculate cutting data to take material differences to hard wood into account. I&amp;rsquo;ll choose a &lt;code&gt;4mm &lt;/code&gt;single flute (&lt;code&gt;z=1&lt;/code&gt;) coated carbide cutter for aluminium and assume a cutting speed &lt;code&gt;vc = 150m/min&lt;/code&gt; for HPL at a feedrate per tooth &lt;code&gt;fTooth = 0,025&lt;/code&gt; similar to cutting mild steel.&lt;/p&gt;&#10;&lt;p&gt;Target RPM: &lt;/p&gt;&#10;$$n=\frac{v_c}{\pi d}=\frac{150000\frac{mm}{min}}{\pi \cdot 4mm}=12000\frac{1}{min}$$&lt;p&gt;Target feedrate: &lt;/p&gt;&#10;$$F=n \cdot f_{Tooth} \cdot z = 12000\frac{1}{min} \cdot 0,025mm \cdot min \cdot 1= 300\frac{mm}{min}$$&lt;p&gt;I chose a 4mm endmill because my router motor torque diminishes over-proportionally when I reduce RPM. With larger endmills, this effect would probably lead to insufficient cutting power. If you have a spindle with an asynchronous machine, your torque curve is essentially flat so that shouldn&amp;rsquo;t be a problem of yours then.&lt;/p&gt;&#10;&lt;p&gt;The feedrate seems so slow - I hope that this wouldn&amp;rsquo;t heat up the material so much that it degrades (it won&amp;rsquo;t melt according to Wikipedia&amp;hellip;)&lt;/p&gt;&#10;&lt;p&gt;For your reference: &lt;a href="https://hobbyline.info/forum/index.php?thread/678-hpl-platten-fr%C3%A4sen/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Here is the related thread&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; in the forum that I am visiting regularly (all in German).&lt;/p&gt;&#10;&lt;h3 id="further-tests"&gt;Further tests&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-06-18_coated_new.jpg" alt="Image: new coated carbide endmill for aluminium"&gt;&lt;/figure&gt;&#10;I have made some parts with the bespoke endmill and settings. I optimized the settings a little for reduced cycle time and ended up with the following values: &lt;code&gt;ftooth = 0.04mm&lt;/code&gt; and &lt;code&gt;vc = 130m/min&lt;/code&gt;. For the 4mm endmill, this resulted in&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;S = 10300RPM&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;F = 410mm/min&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Z+ = 3mm&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;vc = 1%&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;This endmill lived for about 1h. Not much still. Cutting with this kind of bit would never become economical. The HPL I am using has two material properties that will kill carbide cutters quickly. First, it is abrasive so the thin cutter edge is being beaten up. Second, the chips do not carry enough heat away from the cutter edge in time so that their sharpness deteriorates even more quickly.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-06-18_coated_worn.jpg" alt="Image: coated carbide endmill for aluminium after 1h"&gt;&lt;/figure&gt;&#10;&lt;h3 id="final-solution"&gt;Final solution&lt;/h3&gt;&#10;&lt;p&gt;A more economic solution with 40 times (manufacturer statement) more lifetime might be to take a carbide bit with thick diamond blades (e.g. like &lt;a href="https://www.artifex24.de/JSO-5mm-Diamant-PKD-Nutschaftfraeser-Diatec-DUO-5x10/50mm-Z2-S6-rechts" target="_blank" rel="noopener noreferrer" class="external-link"&gt;this one&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;). The downside is that it&amp;rsquo;s almost ten times as expensive. If I have to cut this material more often in the future, I might buy one and share my experience - in the meantime, I&amp;rsquo;ll rather have the cutters sharpened to save money.&lt;/p&gt;&#10;</description></item><item><title>CNC Spoilboard</title><link>https://blog.schallbert.de/en/cnc-spoilboard-diy/</link><pubDate>Sun, 12 Jun 2022</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/cnc-spoilboard-diy/</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 BasicLine0607 CNC. It features a frame so pasts can be positioned repeatedly, and it has a hole grid to allow clamping devices to be inserted."&#10; title="CNC Spoilboard" /&gt;&#10;&lt;h2 id="material-list"&gt;Material list&lt;/h2&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;Count&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;~ Cost [€]&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;MDF, strength 19mm, 840x730&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;30&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;MDF, strength 5mm, 840x730&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;5&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;Drive-in nut&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;17&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;9&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;M6 Machine screw, 20mm + M6 hammer nut + washer&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;20&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;12&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;M6 Machine screw, 35mm + washer&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;17&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5&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;Wood glue&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;-&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0,10&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h2 id="tools"&gt;Tools&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Screw driver / wrench / nut runner&lt;/li&gt;&#10;&lt;li&gt;Your CNC&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="the-build"&gt;The Build&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Download the drawings &lt;a href="https://blog.schallbert.de/assets/docs/basicline0607_spoilboard_drawings_by_schallbert.zip"&gt;here&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Adjust dimensions if needed &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;/li&gt;&#10;&lt;li&gt;Import it to your CAM tool. Provision for different material strength if you have.&lt;/li&gt;&#10;&lt;li&gt;Prepare sunk holes for your fasteners that attach to the machine frame to have some surface-planing reserve.&lt;/li&gt;&#10;&lt;li&gt;As both fence and spoilboard are larger than the CNC&amp;rsquo;s reachable area, you will have to use multiple jobs to complete the tasks. Choose your XY-zero points with care!&lt;/li&gt;&#10;&lt;li&gt;Also note that the slots will have to be cut &amp;ldquo;from the bottom&amp;rdquo; in case you don&amp;rsquo;t have a slot cutter.&lt;/li&gt;&#10;&lt;li&gt;Have your CNC do the work.&lt;/li&gt;&#10;&lt;li&gt;Glue together upper and lower MDF sheets so hammer nuts cannot fall through the slots&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;a href="https://blog.schallbert.de/en/projects/cnc_spoilboard/"&gt;Back to Project main page&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;Note: the center stiffening bar does not have a defined position, so yours might be located somewhere else between its longer counterparts&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>My Endmill 'screams'</title><link>https://blog.schallbert.de/en/my-endmill-screams/</link><pubDate>Mon, 06 Jun 2022</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/my-endmill-screams/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-03-06_endmill-thumb.jpg"&#10; class="post-cover"&#10; alt="Beginner CNC issue: Speeds/Feeds"&#10; title="My Endmill &amp;#39;screams&amp;#39;" /&gt;&#10;&lt;h3 id="disclaimer"&gt;Disclaimer&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-06-03_upcut_bit.jpg" alt="a 2-flute upcut bit for wood"&gt;&lt;/figure&gt;&#10;I&amp;rsquo;m working on my first-ever CNC milling project: A spoilboard that offers holes and t-nuts for workholding purposes and that supports an x-y fence and a tool length sensor.&lt;/p&gt;&#10;&lt;p&gt;I never used a spiral endmill before, but I bought a 6mm 2-cut upcut solid carbide bit with 21mm working length. And it just sounds awful. On my hand-router, I was exclusively using high-speed steel straight flutes and larger bit diameters. They sounded similar to the router itself, a loud but confident hum when cutting through material.&lt;/p&gt;&#10;&lt;h3 id="beginners-confusion"&gt;Beginner&amp;rsquo;s confusion&lt;/h3&gt;&#10;&lt;p&gt;With this upcut bit, I followed the manufacturer&amp;rsquo;s recommendations of feeds and speeds for hard wood on my medium density fiberboard workpiece (depth per pass: &lt;code&gt;6mm&lt;/code&gt;, feed: &lt;code&gt;3500mm/min&lt;/code&gt; speed: &lt;code&gt;24000rpm&lt;/code&gt;). But obviously I do something wrong because there&amp;rsquo;s a terribly loud, high-pitched, uncomfortable screaming/hissing noise when I cut.&lt;/p&gt;&#10;&lt;p&gt;I gradually turned up the feed rate because I was under the impression that screaming bits &amp;ldquo;are not working at full capacity&amp;rdquo;. I ended up with &lt;code&gt;9100mm/min&lt;/code&gt; (!) where it was better, but still&amp;hellip;&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2022-03-06_endmill_screams.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Endmill screams while ploughing through MDF&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h3 id="analysis"&gt;Analysis&lt;/h3&gt;&#10;&lt;p&gt;After the 15min job was finished I touched the endmill - Flutes are as good as new and the bit is barely warm to the touch. Hm. Also the router motor did not sound stressed. My steppers made the machine to literally plough through the MDF (smoothly and without any issue), I even had to turn the extraction up to not drown in dust. Also the result looks good to me; tolerances are tight and the cut is as clean as I&amp;rsquo;d expect it to be - much better than I could have done by hand.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-06-03_mdf_result.jpg" alt="milling in MDF - spoilboard t-nut slots"&gt;&lt;/figure&gt;&#10;&lt;h3 id="open-questions"&gt;Open questions&lt;/h3&gt;&#10;&lt;p&gt;So what am I doing wrong? Is this sound &amp;ldquo;normal&amp;rdquo; for a small spiral flute bit? Does MDF need totally different parameters than hard wood for silent cutting, e.g. much less spindle RPM? Do I need to set a different ploughing strategy in my CAM tool?&lt;/p&gt;&#10;&lt;p&gt;Hold on tight, I&amp;rsquo;ll be checking with a forum to get some advice&amp;hellip;&lt;/p&gt;&#10;&lt;h3 id="questions-answered"&gt;Questions answered&lt;/h3&gt;&#10;&lt;p&gt;OK, I have an answer from the &lt;a href="https://hobbyline.info/forum/index.php?thread/620-mein-fr%C3%A4ser-kreischt/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;bit&amp;rsquo;s manufacturer&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; (in German). This sound is normal for small endmills specialized in cutting wood. Well then. Probaby I should build a housing for my machine and buy some very good earplugs. 😬 Let me quote another tool manufacturer:&lt;/p&gt;&#10;&lt;p&gt;&amp;ldquo;Two-flute cutters are always noisier than single or three-flute cutters of the same size. Their 180° opposing cutting edges lead to additional vibrations of the cutter due to the alternately engaged cutting edges. In return, they offer better wear characteristics than single flute cutters and allow better chip evacuation than triple flute cutters.&amp;rdquo;&lt;/p&gt;&#10;&lt;h3 id="comparison-same-material-different-bit"&gt;Comparison: Same material, different bit&lt;/h3&gt;&#10;&lt;p&gt;Here&amp;rsquo;s a comparison video to judge just how different cutters can sound in the same material:&#10;&lt;code&gt;14mm 3-flute&lt;/code&gt; endmill face cutter, &lt;code&gt;10mm shaft, S5500, F7100, Z0.3, Fside 90%&lt;/code&gt;&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2022-03-06_leveling.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;14mm face cutter calmly levels my spoilboard.&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;Just to test things, I used a spiral strategy in my CAM tool which takes 50% longer than the typical lawn-mower strategies, but this way there are no visible cutter paths on the finished material.&lt;/p&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Field experience&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2024-04-04T00:00:00Z"&gt;&#10; 2024-04-04&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; After round about two years with this wood cutter I can say that they operate robustly. It is advisable to turn up feedrate and depth of cut. Rather more than less also if it requires some bravety (e.g &lt;code&gt;F4000 S24000 Z+12mm&lt;/code&gt; works fine with my machine in Birch plywood). The cutter is quieter when I use conventional milling instead of climb milling.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;</description></item><item><title>CNC Part5 - Macros</title><link>https://blog.schallbert.de/en/macros-for-cnc/</link><pubDate>Wed, 11 May 2022</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/macros-for-cnc/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-06-11_sub-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: snippet of the tool length sensor self-test macro for my CNC machine. It shows C-style code and checks if the probe touches the sensor."&#10; title="CNC Part5 - Macros" /&gt;&#10;&lt;p&gt;This is the 5th part of my portal milling machine sequel. It focuses on handy accessories that make your life as a CNC operator easier, and can lead to more quick and accurate milling results.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/portal-milling/"&gt;&lt;em&gt;Part 1&lt;/em&gt;: Thoughts about CNCs in general and machine selection&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/cnc-electronics/"&gt;&lt;em&gt;Part 2&lt;/em&gt;: CNC Electronics build&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/portal-milling-build/"&gt;&lt;em&gt;Part 3:&lt;/em&gt; CNC Machine build&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/portal-milling-setup/"&gt;&lt;em&gt;Part 4:&lt;/em&gt; CNC Setup&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="overview"&gt;Overview&lt;/h2&gt;&#10;&lt;p&gt;This article is about Macros for the CNC that help automate certain repeating operations. It will both cover basic and more sophisticated Macros for many purposes like automatic Z-zeroing, tool length measurement, or tool changes.&#10;It will touch some &lt;a href="https://www.linuxcnc.org/docs/html/gcode/g-code.html#_g_code_quick_reference_table_a_id_gcode_quick_reference_table_a" target="_blank" rel="noopener noreferrer" class="external-link"&gt;G-code&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; commands and try to help you gain more confidence in your machine.&lt;/p&gt;&#10;&lt;h2 id="disclaimer"&gt;Disclaimer&lt;/h2&gt;&#10;&lt;p&gt;The following subroutines have been written for RS274 NGC interpreter. I have validated them with &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; as that is the CNC software I happen to be using. I have taken some inspiration from the macro file that both &lt;a href="https://webseite.sorotec.de/service/downloads/" 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; and EdingCNC provide along with their machines.&lt;/p&gt;&#10;&lt;p&gt;As you might have different Hardware and Software distributors for your machine, these routines might not fit 100%. My intention was more to explain what&amp;rsquo;s happening in them than to provide a 1-by-1 copy paste template.&lt;/p&gt;&#10;&lt;p&gt;You might want to skip reading this article if your jobs are simple enough so you never felt the need to dive into CNC subroutines or in case your setup is 100% complete and you&amp;rsquo;ll never touch it again.&lt;/p&gt;&#10;&lt;h2 id="so-what-is-a-macro"&gt;So what is a Macro?&lt;/h2&gt;&#10;&lt;p&gt;A Macro or subroutine is a collection of instructions written for the CNC interpreter to perform actions. They are often used to automate recurring operations. On my machine, such a macro starts with a &lt;code&gt;SUB&lt;/code&gt; command and ends with an &lt;code&gt;ENDSUB&lt;/code&gt;. They likely include branching logic and G-code commands to have the machine do things or even show user dialogs to get the operator&amp;rsquo;s input.&lt;/p&gt;&#10;&lt;p&gt;On my machine, all macros are residing in a single file called &lt;code&gt;macro.cnc&lt;/code&gt; that my CNC software will read on startup so it can be executed. Some of the software&amp;rsquo;s buttons even directly call macros from this file, e.g. for homing.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-06-11_macro.jpg" alt="macro execution in action"&gt;&lt;/figure&gt;&#10;To demonstrate that this file matters a lot, close your CNC software and open the &lt;code&gt;macro.cnc&lt;/code&gt; document with a text editor. Search for &lt;code&gt;Sub user_9&lt;/code&gt; (it should be a routine that doesn&amp;rsquo;t do relevant stuff), remove its contents and add the line &lt;code&gt;MSG &amp;quot;hello world!&amp;quot;&lt;/code&gt; to it. Save your work, start the CNC software, and in the user menu, press button &lt;code&gt;9&lt;/code&gt; - Voilà!&lt;/p&gt;&#10;&lt;h2 id="starters-a-simple-macro-to-detect-tool-length-sensor-status"&gt;Starters: A simple macro to detect Tool Length Sensor status&lt;/h2&gt;&#10;&lt;p&gt;Let&amp;rsquo;s get going and connect the newly-grown knowledge about macros to a useful example.&lt;/p&gt;&#10;&lt;h3 id="the-tool-length-sensor"&gt;The tool length sensor&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-06-11_tls3d.jpg" alt="my tool length sensor"&gt;&lt;/figure&gt;&#10;A tool length sensor is a device connected to an input of the CNC machine that triggers when touched (e.g. by a tool tip). There are many different designs available, ranging from a very simple microswitch with an enclosure and touch button to high-precision heavy steel sensors with automatic air purge operation to make sure the sensor&amp;rsquo;s surface is clean.&lt;/p&gt;&#10;&lt;p&gt;The tool length sensor is either mounted at the machine bed, on top of the spoilboard, or just placed at another fixed position on the machine. Mine is a very simple switch that I can put on any place of my machine. It has a claimed repeatability of &lt;code&gt;10µm&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Anyways, the actual magic is done in the CNC software.&lt;/p&gt;&#10;&lt;h3 id="macro-task"&gt;Macro task&lt;/h3&gt;&#10;&lt;p&gt;I want the macro to check whether the tool length sensor is operational (and not stuck). So I verify that the sensor switch&amp;rsquo;s status is &amp;ldquo;not triggered&amp;rdquo; - normally closed. If it was triggered, this could mean the sensor is stuck pushed (happened to me already) or has a broken sensor wire (didn&amp;rsquo;t happen so far), or just that the sensor is not connected to the machine at all.&lt;/p&gt;&#10;&lt;h3 id="macro-code"&gt;Macro code&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-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SUB&lt;/span&gt; is_sensor_ok&#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;sensorStatus &lt;span style="color:#f92672"&gt;==&lt;/span&gt; triggered&lt;span style="color:#f92672"&gt;]&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# 5068 == 0 (normally closed)&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;DLGMSG&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Tool length sensor not connected or already triggered - please check&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; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;dialogButton &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;&#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;sensorStatus &lt;span style="color:#f92672"&gt;==&lt;/span&gt; triggered&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;ERRMSG&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Tool length sensor input still unexpected - aborting&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;ENDIF&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; &lt;span style="color:#66d9ef"&gt;ERRMSG&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;User abort.&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;ENDIF&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;ENDIF&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;ENDSUB&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The Macro&amp;rsquo;s name is &lt;code&gt;is_sensor_ok&lt;/code&gt;. I&amp;rsquo;m checking sensor status and present a dialog message in case something is wrong here. Once the operator presses &amp;ldquo;OK&amp;rdquo;, I&amp;rsquo;m assuming that the problem has been taken care of and try again. If it is still triggered, I&amp;rsquo;ll abort the routine.&lt;/p&gt;&#10;&lt;p&gt;This Macro can be found in all sections below where I need the tool length sensor to measure something - It serves as a guard to not accidentally destroy my machine, the sensor, or the workpiece by interpreting wrong input.&lt;/p&gt;&#10;&lt;h3 id="variable-naming"&gt;Variable naming&lt;/h3&gt;&#10;&lt;p&gt;The RS274/NGC language is old. I mean really old. Its first version was released in the late 1950&amp;rsquo;s. No wonder its parameters (#1 - #5399 is the allowed range) are all numeric both in naming and in the values they are able to store. No type system (like &lt;code&gt;strings&lt;/code&gt;, &lt;code&gt;int&lt;/code&gt; etc.) and no handy naming that makes understanding a parameter easy like &lt;code&gt;sensorStatus&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;It will just be &lt;code&gt;#5068&lt;/code&gt; and you&amp;rsquo;ll have to remember yourself that the CNC software manufacturer selected this variable to flag the tool length sensor&amp;rsquo;s status, and that it is &lt;code&gt;boolean&lt;/code&gt; with &lt;code&gt;0&lt;/code&gt; = not triggered and &lt;code&gt;1&lt;/code&gt; = triggered.&lt;/p&gt;&#10;&lt;p&gt;To make the macro code as readable and comprehensible as possible, I refrain from using numerical parameter names in this article. A translation table can be found &lt;a href="https://blog.schallbert.de/en/macros-for-cnc/#tables"&gt;at the bottom&lt;/a&gt; for your convenience.&lt;/p&gt;&#10;&lt;h2 id="using-the-tool-length-sensor-to-get-workpiece-surface-z0"&gt;Using the tool length sensor to get workpiece surface (Z=0)&lt;/h2&gt;&#10;&lt;p&gt;With this method, you don&amp;rsquo;t need to manually zero in workpiece surfaces by lowering Z-axis until the tool slightly scratches the surface and then setting workpiece coordinates.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-06-11_tls.jpg" alt="tool length sensor sketch"&gt;&lt;/figure&gt;&#10;Once the Macro is programmed and set up, just place the tool length sensor on top of your workpiece and jog your machine so it is placed directly above the sensor (not touching yet). Then start the macro. Your machine will now automatically lower its Z-axis slowly until the sensor switches. It will then reverse very slowly until the sensor untriggers. This point is then taken to determine Z-0 which is then set automatically.&lt;/p&gt;&#10;&lt;h3 id="prerequisites"&gt;Prerequisites&lt;/h3&gt;&#10;&lt;p&gt;What we need before we can write the macro is the tool length sensor&amp;rsquo;s Z-position &lt;code&gt;zTls&lt;/code&gt; at its switching point from being triggered to not triggered. I measured mine with a caliper and noted it down. We need this value so the machine can subtract it from the Z-height when touching off to yield the workpiece surface&amp;rsquo;s height.&lt;/p&gt;&#10;&lt;p&gt;Also, it is important to know how the switch is connected to the machine. The preferred way is &amp;ldquo;normally closed&amp;rdquo;, so the switch opens when triggered: &lt;code&gt;triggered = 0&lt;/code&gt;. This is the more safe application because a broken wire or lost connection is detected automatically as the circuit breaks.&lt;/p&gt;&#10;&lt;p&gt;We&amp;rsquo;ll also have to determine touch probe forward feed and reverse feed, e.g. &lt;code&gt;touch = 100mm/min&lt;/code&gt;, &lt;code&gt;rev = 10mm/min&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;The macro also features a more elaborate part: When Z-0 is about to be measured, but tool length is not known, the machine is able to store current position &lt;code&gt;posX, posY&lt;/code&gt; in repositioning variables so that the &lt;code&gt;get_tool_length&lt;/code&gt; macro can be called directly from here, and later repositionTo to where workpiece Z-0 is being measured.&lt;/p&gt;&#10;&lt;h3 id="the-subroutine-z-zero-detection"&gt;The subroutine &amp;ldquo;Z-zero detection&amp;rdquo;&lt;/h3&gt;&#10;&lt;p&gt;After switching the spindle off, the program will lower the machine&amp;rsquo;s Z-axis until it touches the tool length sensor (or until it has travelled down, spindle nose almost touchting the sensor where it would abort, claiming it didn&amp;rsquo;t find the sensor) via command &lt;code&gt;G38.2&lt;/code&gt;. When triggered, it will reverse carefully until the sensor is released. This point is then saved as the new coordinate offset for the Z-axis via &lt;code&gt;G92&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="code"&gt;Code&lt;/h3&gt;&#10;&lt;p&gt;before we start, make sure we&amp;rsquo;re in the correct state. Tool length should already be determined so future tool changes won&amp;rsquo;t require re-touching off Z-zero. Plus, we should know that the sensor is properly connected.&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-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SUB&lt;/span&gt; measure_workpiece_z0&#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;toolLengthStat&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;DLGMSG&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;WARNING - Please first measure tool length!&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; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;dialogButton &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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; xPosReposition &lt;span style="color:#f92672"&gt;=&lt;/span&gt; xPos&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; yPosReposition &lt;span style="color:#f92672"&gt;=&lt;/span&gt; yPos&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; repositionTo &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; &lt;span style="color:#66d9ef"&gt;GOSUB&lt;/span&gt; get_tool_length&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;ENDIF&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;ENDIF&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;GOSUB&lt;/span&gt; is_sensor_ok ;&lt;span style="color:#75715e"&gt;# Check if tool length sensor status is OK&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Let&amp;rsquo;s now program the actual routine by asking the operator whether Z-zeroing shall be performed now. When the CNC runs in simulator mode (without actual hardware connected), Z-zeroing won&amp;rsquo;t work so we also have to check this.&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-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;DLGMSG&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Start Z-Zeroing?&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; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;dialogButton &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:#66d9ef"&gt;AND&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;operatingMode &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; simulator&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;M5&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;#Switch spindle off&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;M9&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;#Switch coolant off&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;G53&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;G38&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; Z&lt;span style="color:#f92672"&gt;[&lt;/span&gt;zSpindleTip &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt; F&lt;span style="color:#f92672"&gt;[&lt;/span&gt;touch&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:#75715e"&gt;# G38.2 = touch toward probe, stop on contact, flag error on fail&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;# fail = 5mm before touching spindle tip&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;probeOk &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:#75715e"&gt;# #5067 == 1 : G38.2 command success&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;G38&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;G91&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Z20&lt;/span&gt; F&lt;span style="color:#f92672"&gt;[&lt;/span&gt;rev&lt;span style="color:#f92672"&gt;]&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# now reverse slowly to find untrigger point (max. 20mm up)&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;G90&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# back to absolute coordinates&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;probeOk &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; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;G00&lt;/span&gt; Z&lt;span style="color:#f92672"&gt;[&lt;/span&gt;zTouched&lt;span style="color:#f92672"&gt;]&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# #5063 Go to Z-axis&amp;#39;s probe point&#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;G92&lt;/span&gt; Z&lt;span style="color:#f92672"&gt;[&lt;/span&gt;zTls&lt;span style="color:#f92672"&gt;]&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# Set Z-axis&amp;#39;s coordinate offset (0) to tool length sensor&amp;#39;s height&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;G00&lt;/span&gt; Z&lt;span style="color:#f92672"&gt;[&lt;/span&gt;zTls &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# clear sensor&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; &lt;span style="color:#66d9ef"&gt;ERRMSG&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Could not locate sensor untrigger point.&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;ENDIF&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; &lt;span style="color:#66d9ef"&gt;DLGMSG&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Could not locate sensor trigger point. Retry?&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; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;dialogButton &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; &#9;&#9;&#9;&#9;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;GOSUB&lt;/span&gt; measure_workpiece_z0&#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; &lt;span style="color:#66d9ef"&gt;ERRMSG&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;User abort.&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;ENDIF&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;ENDIF&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;ENDSUB&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2022-06-11_tool_length_measurement.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Executing the Get tool length macro on my machine&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;In this video, the machine is configured to return to XY zero when tool measurement has been completed. It would behave the same if I had the tool length measured after a tool change.&lt;/p&gt;&#10;&lt;h2 id="measuring-the-tool-length"&gt;Measuring the tool length&lt;/h2&gt;&#10;&lt;p&gt;If the machine knows the tool length before measuring the Z zero point, there is no need to re-determine the Z zero point during tool changes that occur whilst the job is in progress. I describe the relevant macro below.&lt;/p&gt;&#10;&lt;h3 id="preparation"&gt;Preparation&lt;/h3&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-06-11_toolLengthVariables.jpg" alt="Image: get_tool_length macro parameters"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Most CNCs derive their current position data by counting the motor steps they have executed after completing the home run. To measure the tool length, a few programmed parameters are required:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Z-position of the spindle nose &lt;code&gt;zSpindleTip&lt;/code&gt; (touches the tool length sensor without a tool inserted) so that the distance the tool protrudes from the spindle can be calculated&lt;/li&gt;&#10;&lt;li&gt;X-position &lt;code&gt;xPosTls&lt;/code&gt; of the tool length sensor during tool changes&lt;/li&gt;&#10;&lt;li&gt;Y-position &lt;code&gt;yPosTls&lt;/code&gt; of the tool length sensor during tool changes&lt;/li&gt;&#10;&lt;li&gt;Safety height &lt;code&gt;zSafety&lt;/code&gt; (ensures that the spindle does not encounter an obstacle on its way to the tool change area)&lt;/li&gt;&#10;&lt;li&gt;Probing feed rate forwards and backwards. We can reuse this from the macro for the workpiece zero point.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="the-measure-tool-length-subroutine"&gt;The Measure tool length subroutine&lt;/h3&gt;&#10;&lt;p&gt;Let us first define what this subroutine is intended to do:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Stop the spindle, coolant, etc.&lt;/li&gt;&#10;&lt;li&gt;Dialogue: The operator should enter the estimated tool length&lt;/li&gt;&#10;&lt;li&gt;Rapid traverse Z to a safe height, then XY to the position of the tool sensor&lt;/li&gt;&#10;&lt;li&gt;Probe the tool length sensor, similar to workpiece zero point determination&lt;/li&gt;&#10;&lt;li&gt;Calculate the tool length&lt;/li&gt;&#10;&lt;li&gt;Calculate the difference between the last and current tool lengths&lt;/li&gt;&#10;&lt;li&gt;Update the Z zero point accordingly&lt;/li&gt;&#10;&lt;li&gt;Return Z to the safety height in rapid traverse&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="function-measure-tool-length"&gt;Code: Measure tool&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-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SUB&lt;/span&gt; get_tool_length&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;GOSUB&lt;/span&gt; is_sensor_ok ;&lt;span style="color:#75715e"&gt;# Check if tool length sensor status is OK&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;DLGMSG&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Start tool length measurement? Please enter estimated tool length: &amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;toolLengthEst&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; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;dialogButton &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:#66d9ef"&gt;AND&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;operatingMode &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; simulator&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; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;toolLengthEst &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&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;ERRMSG&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Error: tool length cannot be negative.&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;ENDIF&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;zSpindleTip&lt;span style="color:#f92672"&gt;+&lt;/span&gt; toolLengthEst &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;zSafety&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;ERRMSG&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Error: tool too long - could collide with sensor.&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;ENDIF&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;M5&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# Switch spindle off&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;M9&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# Switch coolant off&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;G53&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;G00&lt;/span&gt; Z&lt;span style="color:#f92672"&gt;[&lt;/span&gt;zSafety&lt;span style="color:#f92672"&gt;]&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# Go to safety height (machine coordinates)&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;G53&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;G00&lt;/span&gt; X&lt;span style="color:#f92672"&gt;[&lt;/span&gt;xPosTls&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Y&lt;span style="color:#f92672"&gt;[&lt;/span&gt;xPosTls&lt;span style="color:#f92672"&gt;]&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# Go to tool length sensor&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;G53&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;G00&lt;/span&gt; Z&lt;span style="color:#f92672"&gt;[&lt;/span&gt;zSpindleTip &lt;span style="color:#f92672"&gt;+&lt;/span&gt; toolLengthEst &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# Move Z down to 10mm above estimated tool tip&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;# measure tool length, save results, apply Z-offset if needed&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;G53&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;G38&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; Z&lt;span style="color:#f92672"&gt;[&lt;/span&gt;zSpindleTip&lt;span style="color:#f92672"&gt;]&lt;/span&gt; F&lt;span style="color:#f92672"&gt;[&lt;/span&gt;touch&lt;span style="color:#f92672"&gt;]&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# probe sensor, latest stop point is spindle tip&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;probeOk &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:#75715e"&gt;# #5067 == 1 : G38.2 command success&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;G38&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;G91&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Z20&lt;/span&gt; F&lt;span style="color:#f92672"&gt;[&lt;/span&gt;rev&lt;span style="color:#f92672"&gt;]&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# now reverse slowly to find untrigger point&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;G90&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# back to absolute coordinates&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;probeOk &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; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; toolLength &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;zTouched &lt;span style="color:#f92672"&gt;-&lt;/span&gt; sTip&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;MSG&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Tool length = &amp;#34;&lt;/span&gt; toolLength&#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;toolLengthStat &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:#75715e"&gt;# defaults to 0 on startup&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lastToolLength &lt;span style="color:#f92672"&gt;=&lt;/span&gt; currToolLength ;&lt;span style="color:#75715e"&gt;# save last tool&amp;#39;s length&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; currToolLength &lt;span style="color:#f92672"&gt;=&lt;/span&gt; toolLength ;&lt;span style="color:#75715e"&gt;# save current tool&amp;#39;s length&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; toolLengthDiff &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;currToolLength &lt;span style="color:#f92672"&gt;-&lt;/span&gt; lastToolLength&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#9;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;G92&lt;/span&gt; Z&lt;span style="color:#f92672"&gt;[&lt;/span&gt;zPos &lt;span style="color:#f92672"&gt;-&lt;/span&gt; toolLengthDiff&lt;span style="color:#f92672"&gt;]&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# Set Z-axis&amp;#39;s coordinate offset (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;ELSE&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; currToolLength &lt;span style="color:#f92672"&gt;=&lt;/span&gt; toolLength ;&lt;span style="color:#75715e"&gt;# save current tool&amp;#39;s length&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;ENDIF&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; toolLengthStat &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; &lt;span style="color:#66d9ef"&gt;GOSUB&lt;/span&gt; reposition_spindle&#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; &lt;span style="color:#66d9ef"&gt;ERRMSG&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Could not locate sensor untrigger point.&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;ENDIF&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; &lt;span style="color:#66d9ef"&gt;ERRMSG&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Could not locate sensor trigger point.&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;ENDIF&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;ENDIF&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;ENDSUB&lt;/span&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2022-06-11_tool_length_measurement.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Determining tool length on my machine&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;My machine is configured to return to workpiece coordinates &lt;code&gt;XY0&lt;/code&gt; when done with tool length measurement.&lt;/p&gt;&#10;&lt;h2 id="tool-change"&gt;Tool change&lt;/h2&gt;&#10;&lt;p&gt;When there is no macro for tool changes, the machine will pause its job and wait until you manually jogged it to where you perform the tool change, and requires that you re-zero the workiece&amp;rsquo;s Z0 position due to possibly changed tool length before continuing the job.&lt;/p&gt;&#10;&lt;p&gt;This task can easily be automated and the following section guides you how to write a Macro for this.&lt;/p&gt;&#10;&lt;h3 id="prerequisites-1"&gt;Prerequisites&lt;/h3&gt;&#10;&lt;p&gt;An optional flag &lt;code&gt;getToolLength&lt;/code&gt; could be configuring the tool change macro&amp;rsquo;s behavior - whether every tool&amp;rsquo;s length should be measured after a tool change or not. If you have an automatic tool changer, you might not need this to happen.&lt;/p&gt;&#10;&lt;p&gt;The following additional parameters are also needed&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;X-Position &lt;code&gt;xToolChg&lt;/code&gt; where the tool change takes place (for me it&amp;rsquo;s &lt;code&gt;xToolChg = xPosTls&lt;/code&gt;)&lt;/li&gt;&#10;&lt;li&gt;Y-Position &lt;code&gt;yToolChg&lt;/code&gt; where the tool change takes place (for me it&amp;rsquo;s &lt;code&gt;yToolChg = yPosTls&lt;/code&gt;)&lt;/li&gt;&#10;&lt;li&gt;re-use &lt;code&gt;zSafety&lt;/code&gt; from &lt;code&gt;get_tool_length&lt;/code&gt; macro&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;newToolNumber&lt;/code&gt; to indicate the requested tool from G-code (or for manual input)&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;currToolNumber&lt;/code&gt; to indicate the &amp;ldquo;old&amp;rdquo; tool to be replaced&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;toolChangeDone&lt;/code&gt; helper flag to indicate whether a tool change has taken place yet&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="the-subroutine-change-tool"&gt;The subroutine &amp;ldquo;Change Tool&amp;rdquo;&lt;/h3&gt;&#10;&lt;p&gt;The following steps are being performed by the macro when a tool change is indicated either by command &lt;code&gt;M06&lt;/code&gt; &amp;ldquo;Tool change&amp;rdquo; within the G-code file of a job or triggered manually by the user:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Stop spindle, coolant etc.&lt;/li&gt;&#10;&lt;li&gt;If requested tool is the current tool, prompt dialog asking if it should anyways change.&lt;/li&gt;&#10;&lt;li&gt;Rapid move Z up (&lt;code&gt;zSafety&lt;/code&gt;), then XY to the tool change position&lt;/li&gt;&#10;&lt;li&gt;Dialog: State current tool &lt;code&gt;a&lt;/code&gt; and request to insert requested tool &lt;code&gt;b&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Check if tool change configuration implies tool length determination. If so, call &lt;code&gt;get_tool_length&lt;/code&gt; macro.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="code-1"&gt;Code&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-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SUB&lt;/span&gt; change_tool&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; toolChangeDone &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;M5&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# Switch spindle off&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;M9&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# Switch coolant off&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;operatingMode &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; simulator&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;TCAGuard&lt;/span&gt; off ;&lt;span style="color:#75715e"&gt;# tool change area guard: off for tool change&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;# handle case that tool is already in place&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;newToolNumber &lt;span style="color:#f92672"&gt;==&lt;/span&gt; currToolNumber&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;DLGMSG&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Tool already mounted. Change anyways?&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; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;dialogButton &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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; toolChangeDone &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;ELSE&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; toolChangeDone &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; &lt;span style="color:#66d9ef"&gt;ENDIF&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;ENDIF&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;# go to tool change position and prompt to change tool&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;toolChangeDone &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&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;G53&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;G00&lt;/span&gt; Z&lt;span style="color:#f92672"&gt;[&lt;/span&gt;zSafety&lt;span style="color:#f92672"&gt;]&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# Go to safety height (machine coordinates)&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;G53&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;G00&lt;/span&gt; X&lt;span style="color:#f92672"&gt;[&lt;/span&gt;xToolChg&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Y&lt;span style="color:#f92672"&gt;[&lt;/span&gt;yToolChg&lt;span style="color:#f92672"&gt;]&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# Go to tool change position&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;DLGMSG&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Please mount tool now. Old tool number: &amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;currToolNumber&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:#e6db74"&gt;&amp;#34; New tool number: &amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;newToolNumber&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; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;dialogButton &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;&#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;newToolNumber &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;99&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;OR&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;newToolNumber &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&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;TCAGuard&lt;/span&gt; on ;&lt;span style="color:#75715e"&gt;# tool change area guard: on for normal job&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;ERRMSG&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;New tool number implausible.&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;ENDIF&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; toolChangeDone &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; &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; &lt;span style="color:#66d9ef"&gt;ERRMSG&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Tool change aborted.&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;ENDIF&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;ENDIF&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;# prompt when complete and optionally call tool length measurement&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;toolChangeDone &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; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;MSG&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Tool change from &amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;currToolNumber&lt;span style="color:#f92672"&gt;]&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34; to &amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;newToolNumber&lt;span style="color:#f92672"&gt;]&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34; complete.&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;M6&lt;/span&gt; T&lt;span style="color:#f92672"&gt;[&lt;/span&gt;newToolNumber&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#9;&#9;&#9;&#9; ;&lt;span style="color:#75715e"&gt;# set new tool number&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;getToolLength &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:#75715e"&gt;# config flag 0 = no, 1 = yes&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;GOSUB&lt;/span&gt; get_tool_length ;&lt;span style="color:#75715e"&gt;# Measure tool length. Careful: To be called after M6 T !&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; &lt;span style="color:#66d9ef"&gt;GOSUB&lt;/span&gt; reposition_spindle&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;ENDIF&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;ENDIF&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;TCAGuard&lt;/span&gt; on ;&lt;span style="color:#75715e"&gt;# tool change area guard: on for normal job&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;ENDIF&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;ENDSUB&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="reposition-to-saved-coordinates"&gt;Reposition (to saved coordinates)&lt;/h2&gt;&#10;&lt;p&gt;This is a very short macro that can be called from other subroutines to reposition the machine either to a commanded position or to XY workpiece zero. It could be further enhanced with more &lt;code&gt;repositionTo&lt;/code&gt; flag values, e.g.&#10;0 = no repositioning, 1 = custom position, 2 = workpiece zero, 3 = machine zero &amp;hellip;&lt;/p&gt;&#10;&lt;h3 id="code-2"&gt;Code&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-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SUB&lt;/span&gt; reposition_spindle&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;G53&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;G00&lt;/span&gt; Z&lt;span style="color:#f92672"&gt;[&lt;/span&gt;zSafety&lt;span style="color:#f92672"&gt;]&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# Go to safety height (machine coordinates)&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;repositionTo &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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;G00&lt;/span&gt; X&lt;span style="color:#f92672"&gt;[&lt;/span&gt;xPosReposition&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Y&lt;span style="color:#f92672"&gt;[&lt;/span&gt;yPosReposition&lt;span style="color:#f92672"&gt;]&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# Move back to where requested before&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; repositionTo &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# reset reposition flag and values&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; xPosReposition &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; yPosReposition &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;ELSE&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;G00&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;X0&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Y0&lt;/span&gt; ;&lt;span style="color:#75715e"&gt;# Move back to XY zero (workpiece coordinates)&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;ENDIF&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;ENDSUB&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="tables"&gt;Table of variables&lt;/h2&gt;&#10;&lt;p&gt;In the following sections you can find all parameters I used in the macros above.&lt;/p&gt;&#10;&lt;h3 id="system-parameters"&gt;System parameters&lt;/h3&gt;&#10;&lt;p&gt;Protected means that these parameters belong to fixed commands or states that are write-protected, and Reserved means parameters have a fix usage within my CNC software (don&amp;rsquo;t know about other CNC programs).&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;Variable name&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Parameter nr.&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Type&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Comment&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;xPos&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#5001&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;protected&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;current CNC position&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;yPos&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#5002&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;protected&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;current CNC position&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;zPos&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#5003&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;protected&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;current CNC position&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;currToolNumber&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#5008&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;protected&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;[1&amp;hellip;99]&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;newToolNumber&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#5011&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;protected&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;[1&amp;hellip;99]&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;zTouched&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#5063&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;protected&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Z where sensor touched&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;probeOk&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#5067&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;protected&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1 = OK&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;sensorStatus&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#5068&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;protected&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1 = triggered&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;operatingMode&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#5397&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;reserved&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1 = simulator&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;dialogButton&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#5398&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;reserved&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1 = OK&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="config-parameters"&gt;Config parameters&lt;/h3&gt;&#10;&lt;p&gt;Config parameters are the one that are set once and then kept constant as they are tied to the CNC and their geometry.&lt;/p&gt;&#10;&lt;p&gt;Volatility of parameters heavily depends on the CNC software solution you&amp;rsquo;re using, the numbers of your Free parameters might differ. In my software, variables in the range &lt;code&gt;#4000 - #4999&lt;/code&gt; are persisted while all other free and reserved parameters are volatile and/or scoped.&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;Variable name&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Parameter nr.&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Type&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Comment&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;triggered&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#4400&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;free, persisted&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1 = normally open&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;currToolLength&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#4501&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;free, persisted&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;[mm]&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;lastToolLength&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#4502&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;free, persisted&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;[mm]&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;zSafety&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#4506&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;free, persisted&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Z position for move&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;xPosTls&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#4507&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;free, persisted&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;tls position&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;xPosTls&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#4508&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;free, persisted&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;tls position&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;zSpindleTip&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#4509&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;free, persisted&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Z zpindle on tls&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;zTls&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#4510&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;free, persisted&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;TLS height [mm]&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;toolLengthEst&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#4511&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;free, persisted&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;[mm]&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;touch&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#4512&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;free, persisted&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;touch feed [mm/min]&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;rev&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#4513&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;free, persisted&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;reverse feed [mm/min]&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;getToolLength&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#4520&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;free, persisted&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Flag, 1 = Yes&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;xToolChg&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#4521&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;free, persisted&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;tool change position&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;yToolChg&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#4522&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;free, persisted&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;tool change position&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="flag-parameters"&gt;Flag parameters&lt;/h3&gt;&#10;&lt;p&gt;Free parameters can be used as wished by the programmer. Care has to be taken not to accidentally re-use an existing system variable, though.&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;Variable name&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Parameter nr.&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Type&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Comment&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;toolLengthStat&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#3501&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;free&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1 = measured&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;toolLengthDiff&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#3502&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;free&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;[mm]&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;toolChangeDone&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#5015&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;free&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1 = Yes&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;repositionTo&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#5020&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;free&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Flag, 1 = Yes&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;xPosReposition&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#5021&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;free&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;reposition flag&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;yPosReposition&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#5022&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;free&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;reposition flag&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;toolLength&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;#5024&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;free&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;[mm]&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;</description></item><item><title>CNC Part 4.1 - Config</title><link>https://blog.schallbert.de/en/edingcnc-config/</link><pubDate>Tue, 08 Mar 2022</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/edingcnc-config/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-03-08_edingcnc-thumb.jpg"&#10; class="post-cover"&#10; alt="Zerspanobert&amp;#39;s switchbox, components labeled"&#10; title="CNC Part 4.1 - Config" /&gt;&#10;&lt;p&gt;This is part 4.1 of my portal milling sequel. It is tightly coupled to &lt;a href="https://blog.schallbert.de/en/portal-milling-setup/"&gt;Part4&lt;/a&gt; but specifically handles settings for the CNC software that I use.&lt;/p&gt;&#10;&lt;h2 id="hardware-assumptions"&gt;Hardware Assumptions&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://edingcnc.com/product/cpu5a4e/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;EdingCnc V5A-4E&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; processor board or similar&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.benezan-electronics.de/index.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Interface Advanced PRO&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; breakout board or similar&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.upload.sorotec.de/doku/manuals/adapter.pdf" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Adapter Board&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; or similar&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;These components are necessary to translate the commands created by the CNC software into physical-world signals that can actually interact with matter. The breakout board is controlled by the CNC processor and provides relay outputs for the router and my dust collector so I can have them switched on using my laptop. There are also analog voltage outputs e.g. to control spindle RPM and multiple inputs e.g. to read reference switch status and for signaling events back to the CNC software.&lt;/p&gt;&#10;&lt;p&gt;Of course there are many manufacturers of these circuits out there, but I happen to be using the above ecosystem so I can only speak for those. The underlying technology is the same, and although you might have a different user interface or communication channel like USB, your software will likely provide similar if not identical settings.&lt;/p&gt;&#10;&lt;h2 id="setup-screen-1"&gt;Setup screen 1&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-03-08_settings1.jpg" alt="EdingCNC settings screen1"&gt;&lt;/figure&gt;&#10;&lt;h3 id="kinematics"&gt;Kinematics&lt;/h3&gt;&#10;&lt;p&gt;As discussed in-depth &lt;a href="https://blog.schallbert.de/en/portal-milling-setup/#kinematics"&gt;in this post&lt;/a&gt;, the green frame shows kinematics settings. A negative sign in the &lt;code&gt;Steps/[mm]&lt;/code&gt; indicates that the stepper motor direction is inverted. Note that the &lt;code&gt;reversing play&lt;/code&gt; field can remain 0 when ball screws are used. I&amp;rsquo;m using &amp;ldquo;simple kinematics&amp;rdquo; so the coordinate system is cartesian.&lt;/p&gt;&#10;&lt;h3 id="reference--emergency-stop"&gt;Reference / Emergency stop&lt;/h3&gt;&#10;&lt;p&gt;This section controls how the software interptets signal levels for input switches on the interface board. Although I&amp;rsquo;m sure I wired all inputs &amp;ldquo;normally closed&amp;rdquo; to avoid undetected cable rupture, I had to invert the input for the emergency switch.&lt;/p&gt;&#10;&lt;h3 id="spindle"&gt;Spindle&lt;/h3&gt;&#10;&lt;p&gt;These settings are very specific to the machine you&amp;rsquo;re using. I wanted the spindle to stop when I pause a job, and to automatically restart before continuing the job.&lt;/p&gt;&#10;&lt;p&gt;The &lt;code&gt;approachFeed&lt;/code&gt;parameter is a bit confusing because the default value is really low here. Its meaning is the following: It&amp;rsquo;s the Z-axis&amp;rsquo;s downwards feed when a job had been paused and the Z-axis moved upwards. When you press &amp;ldquo;continue&amp;rdquo;, the Z-axis will be lowered with this feed and when at target depth, the job will continue normally. I entered &lt;code&gt;600mm/min&lt;/code&gt; as a value most endmills should be capable of dealing with.&lt;/p&gt;&#10;&lt;h2 id="setup-screen-2"&gt;Setup screen 2&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-03-08_settings2.jpg" alt="EdingCNC settings screen2"&gt;&lt;/figure&gt;&#10;&lt;h3 id="user-interface"&gt;User interface&lt;/h3&gt;&#10;&lt;p&gt;I wanted referencing/homing to be mandatory before being able to load a job or to rapid-move the machine&amp;rsquo;s axis.&#10;I also selected &lt;code&gt;automatic tool change&lt;/code&gt; although I don&amp;rsquo;t have a tool changer because otherwise my tool change macro helping me do the manual tool change wouldn&amp;rsquo;t be called.&lt;/p&gt;&#10;&lt;h3 id="interface-io"&gt;Interface I/O&lt;/h3&gt;&#10;&lt;p&gt;To account for the interface/ breakout board&amp;rsquo;s hardware setup, I had to invert some software outputs so they&amp;rsquo;d do what I&amp;rsquo;d expect them to. If I didn&amp;rsquo;t invert &lt;code&gt;PWM1&lt;/code&gt; for example, the spindle would start at full speed when I just wanted &lt;code&gt;5000rpm&lt;/code&gt; and vice versa.&lt;/p&gt;&#10;&lt;h2 id="variables-screen"&gt;Variables screen&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-03-08_variables.jpg" alt="EdingCNC variables screen"&gt;&lt;/figure&gt;&#10;&lt;h3 id="parking-position-g28--g30"&gt;Parking position G28 / G30&lt;/h3&gt;&#10;&lt;p&gt;I set my parking positions in a way that allow me to work more efficiently. One of these commands would make the axes move to XY-zero of my fence that I&amp;rsquo;d also make workpiece-zero in CAM so zeroing in becomes really easy.&lt;/p&gt;&#10;&lt;p&gt;The other one is set to a point where the reference switches of all three axes are nearly pressed so homing the machine after startup becomes both easy and quick.&lt;/p&gt;&#10;&lt;h3 id="variables-monitoring"&gt;Variables monitoring&lt;/h3&gt;&#10;&lt;p&gt;Note the monitoring window. You can enter a variable from the RS274 NGC interpreter range and have their current values displayed here. This comes in handy when you want to debug macros or just want to read internal parameters like measured tool length etc.&lt;/p&gt;&#10;</description></item><item><title>CNC Part4 - Setup</title><link>https://blog.schallbert.de/en/portal-milling-setup/</link><pubDate>Thu, 03 Mar 2022</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/portal-milling-setup/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-03-03_setup-thumb.jpg"&#10; class="post-cover"&#10; alt="First steps with CNC software &amp; config"&#10; title="CNC Part4 - Setup" /&gt;&#10;&lt;p&gt;This is the 4th part of my portal milling machine sequel. It concentrates on hardware and software setup so the portal milling machine &amp;ldquo;Zerspanobert&amp;rdquo; can be soon switched into productive mode.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/portal-milling/"&gt;&lt;em&gt;Part 1&lt;/em&gt;: Thoughts about CNCs in general and machine selection&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/cnc-electronics/"&gt;&lt;em&gt;Part 2&lt;/em&gt;: CNC Electronics build&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/portal-milling-build/"&gt;&lt;em&gt;Part 3:&lt;/em&gt; CNC Machine build&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="kinematics"&gt;Kinematics&lt;/h2&gt;&#10;&lt;p&gt;Before the CNC software can do any job on my &lt;strong&gt;BasicLine 0607&lt;/strong&gt;, it needs to be set up correctly so it knows the mechanical ranges of the machines&amp;rsquo; axes, where the homing switches are located, and in which direction to turn the stepper motors to yield &amp;ldquo;positive/negative&amp;rdquo; axis movement.&lt;/p&gt;&#10;&lt;p&gt;As a first step, a decision is needed on the machine&amp;rsquo;s axis naming (X, Y, Z), on where all axes shall effectively be 0, and movement direction (positive, negative). All those coordinate points are relative and can be chosen arbitrarily, but there are some conventions in the CNC world:&lt;/p&gt;&#10;&lt;p&gt;Axes:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;X-Axis is on the portal bed, moving the portal&amp;rsquo;s Y-axis.&lt;/li&gt;&#10;&lt;li&gt;Y-Axis is up on the portal, moving the Z-axis assembly.&lt;/li&gt;&#10;&lt;li&gt;Z-Axis is located at the spindle, moving it up- and downwards.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="machine-0-point"&gt;Machine 0-point&lt;/h3&gt;&#10;&lt;p&gt;Machine 0 and movement direction:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;X-0 is at the &amp;ldquo;rear&amp;rdquo; of the axis. Movement direction is negative.&lt;/li&gt;&#10;&lt;li&gt;Y-0 is at the &amp;ldquo;left&amp;rdquo; of the axis. Movement direction is positive.&lt;/li&gt;&#10;&lt;li&gt;Z-0 is at the &amp;ldquo;top&amp;rdquo; of the axis. Movement direction is negative.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;For my machine and the room setup I have, I chose the X-axis to be the portal&amp;rsquo;s long axis while the Y-axis would be along the linear guides of the machine bed. Z would be facing upwards, following the convention. I wanted the machine Zero to be in the &amp;ldquo;Top left corner&amp;rdquo;, so Z-axis would go down (-), Y-Axis down (-), X-Axis to the right (+).&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-03-03_kinematics.jpg" alt="machine kinematics and 0-point"&gt;&lt;/figure&gt;&#10;&lt;h3 id="movement-directions"&gt;Movement directions&lt;/h3&gt;&#10;&lt;p&gt;To get started with the configuration, I connected to &lt;a href="https://edingcnc.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;my CNC software&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. In the &lt;em&gt;Setup&lt;/em&gt; view, I entered the step count needed to move an axis by one millimeter: &lt;code&gt;16 microsteps/step&lt;/code&gt; multiplied by &lt;code&gt;200 steps/turn&lt;/code&gt; divided by &lt;code&gt;10mm/screw turn&lt;/code&gt; = &lt;code&gt;320 microsteps/mm&lt;/code&gt;. I took double this value for the Z-Axis as the ball screw thread is less steep here.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;16 microsteps/step&lt;/code&gt; is the stock value recommended by the machine manufacturer. There are &lt;a href="https://www.faulhaber.com/en/support/technical-support/motors/tutorials/stepper-motor-tutorial-microstepping-myths-and-realities/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;some good arguments&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and an in-depth analysis &lt;a href="https://www.edn.com/why-microstepping-in-stepper-motors-isnt-as-good-as-you-think/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;here&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; that recommend to use less microstepping, though. As I&amp;rsquo;ll neither require &lt;code&gt;1/100mm&lt;/code&gt; accuracy nor see extremely high cutting forces due to the materials I&amp;rsquo;ll be working with, I just kept the stock settings here.&lt;/p&gt;&#10;&lt;p&gt;Without homing (which wouldn&amp;rsquo;t work yet anyways), I used the jog keys at safety speed to check which key would move which axis, and into which direction. As my setup is different from &amp;ldquo;normal&amp;rdquo; the values I chose may differ from yours.&#10;If an axis wouldn&amp;rsquo;t match the arrow-key (e.g. pressing &amp;ldquo;right&amp;rdquo; and I&amp;rsquo;d expect my X-axis to move to the right, but actually the Z-axis would go down), I&amp;rsquo;d switch connections at the switch box until the keys would match the expected axes.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-03-03_setup_kinematics.jpg" alt="machine kinematics EdingCNC setup"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Now, if the keys would move the axis into the wrong way, e.g I press &amp;ldquo;page down&amp;rdquo; but Z-Axis would go up, I&amp;rsquo;d change the signedness of the resolution field. For the axes I now had:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;X-axis -320.000&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Y-axis 320.000&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Z-axis -640.000&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="machine-travel"&gt;Machine travel&lt;/h3&gt;&#10;&lt;p&gt;My machine has a usable mechanical travel of &lt;code&gt;0 / 732mm&lt;/code&gt; in X direction, &lt;code&gt;-654mm / 0&lt;/code&gt; in Y direction, and &lt;code&gt;-134mm / 0&lt;/code&gt; in Z direction. I entered these values into the mask for &lt;code&gt;negative limit&lt;/code&gt; / &lt;code&gt;positive limit&lt;/code&gt; and set the signedness based on the movement direction relative to &lt;a href="https://blog.schallbert.de/en/portal-milling-setup/#machine-0-point"&gt;machine zero&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="reference-switches-trigger-level"&gt;Reference switches: Trigger level&lt;/h3&gt;&#10;&lt;p&gt;The axes of most machines have a &lt;a href="https://cncphilosophy.com/cnc-offsets-and-machine-reference-explained/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;reference switch&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. These switches are used to &amp;ldquo;home&amp;rdquo; the machine. Homing is required at every machine startup because internally, the software is just inferring XYZ-position by accumulating motor steps. Once the system is restarted, all those values go back to 0 so the machine forgets its position.&lt;/p&gt;&#10;&lt;p&gt;I connected my switches so they are &amp;ldquo;Normally Closed&amp;rdquo;: Their contacts are conducting current when not triggered. This way, a faulty open connection can be detected immediately as the machine would home (without success) at an unusually low speed in this case: &lt;code&gt;homeVelocitySlow&lt;/code&gt; is just 1/10 of normal homing speed in my config. Still, we need to make sure that the homing contact is interpreted correctly. This can be set in the &amp;ldquo;Homing and E-Stop&amp;rdquo; section of the &amp;ldquo;Setup&amp;rdquo; screen. I would enter a &lt;code&gt;1&lt;/code&gt; for &lt;code&gt;normally closed&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="reference-switches-setup"&gt;Reference switches: Setup&lt;/h3&gt;&#10;&lt;p&gt;Some CNCs provide a closed-loop positioning control - they don&amp;rsquo;t need reference switches because their stepper motors are equipped with position encoders that feedback the absolute position of each axis. These machines will not lose steps and are able to detect positioning errors, e.g. caused during mechanical overload.&lt;/p&gt;&#10;&lt;p&gt;Using the reference switch, a machine without closed-loop positioning can reset its step count at a known position so that axes movement becomes repeatable. Still, the reference switch position has to be known to correctly relate step count to axes positions, and that&amp;rsquo;s why we need to enter the reference switches&amp;rsquo; positions on the axes. I just watched by how far I could move the machine (Jog menu) after the reference switch has been triggered before I hit an axis&amp;rsquo; mechanical end. These are the values I finally entered for my model:&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;Steps/AppUnit&lt;/em&gt;&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;X-axis: 10mm&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Y-axis: -4mm&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;Z-axis: -16mm&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="emergency-stop"&gt;Emergency Stop&lt;/h3&gt;&#10;&lt;p&gt;The emergency stop button is just another input for both the CNC software and hardware. Pressing this button will stop axis motion and deactivate both spindle, cooling, and auxiliary circuits. It is also wired &amp;ldquo;normally closed&amp;rdquo; to detect connection issues and can be configured like a reference switch.&lt;/p&gt;&#10;&lt;p&gt;When the emergency stop has been activated, the machine&amp;rsquo;s drivers have to be reset and the machine must be homed again before it can continue normal operation after the button has been released again.&lt;/p&gt;&#10;&lt;h2 id="axis-motion-parameters"&gt;Axis motion parameters&lt;/h2&gt;&#10;&lt;p&gt;The CNC software needs to know how to correctly handle motion of the machine&amp;rsquo;s axes: a heavier and stiffer machine with strong steppers naturally will be able to run more quickly and handle hard accelerations / jerks better than softer, lighter machines under high axial load conditions during cutting. This is why motion parameters of your machine should be carefully identified and deposited in the software.&lt;/p&gt;&#10;&lt;h3 id="axis-speed"&gt;Axis speed&lt;/h3&gt;&#10;&lt;p&gt;&amp;ldquo;Why should Zerspanobert be fast?&amp;rdquo; I asked myself. I didn&amp;rsquo;t come up with too many good answers as normal feed rates with the endmills I plan to be using will remain far below the machine&amp;rsquo;s capabilities. Nevermind, here&amp;rsquo;s some arguments to make a machine quick:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;job times should not become too long&lt;/li&gt;&#10;&lt;li&gt;high travel distance to be covered on a large machine&lt;/li&gt;&#10;&lt;li&gt;when cutting soft, low-density materials, feeds may be much higher&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Still want a quick machine? Some calculations will help us here.&lt;/p&gt;&#10;&lt;h4 id="determining-motion-electronics-capabilities"&gt;Determining motion electronics capabilities&lt;/h4&gt;&#10;&lt;p&gt;The maximum speed is limited by multiple factors:&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;Parameter&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Symbol&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Value&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Unit&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;max drive frequency&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;fD&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;125&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;kHz&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;drive microstep setting&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;N&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;16&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&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;max software frequency&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;fS&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;125&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;kHz&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;stepper steps/turn&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;nS&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;200&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&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;stepper max rpm&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;nSm&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;3300&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1/min&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;ball screw thread width&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;P&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0.01&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;m&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;ball screw max rpm&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;nLm&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1700&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1/min&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;Let&amp;rsquo;s take an example for a &lt;code&gt;125kHz&lt;/code&gt; driver and assume that the software supports that step frequency. I further assume the motors have &lt;code&gt;200 steps/turn&lt;/code&gt; and your driver is configured to &lt;code&gt;16 microsteps/step&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;This gives you a maximum speed of \(\frac{125000}{3200} = 39\) rotations per second or &lt;code&gt;2340 RPM&lt;/code&gt; which is quick for a stepper motor. On a &lt;code&gt;10mm&lt;/code&gt; thread, this would lead to a machine max speed of &lt;code&gt;23.4m/min&lt;/code&gt; which is really, really fast (for non-professional grade machines this guide is targeting).&lt;/p&gt;&#10;&lt;h4 id="checking-mechanical-limitations"&gt;Checking mechanical limitations&lt;/h4&gt;&#10;&lt;p&gt;Ok, let&amp;rsquo;s see if that&amp;rsquo;s acceptable for our ball screws - their allowable RPM is determined by series (&lt;code&gt;KGS16&lt;/code&gt; in my case) and unsupported length (&lt;code&gt;~850mm&lt;/code&gt;). The value I read from a &lt;a href="https://www.neff-gewindetriebe.de/fileadmin/PDF-Datenblaetter/berechnungsgrundlagen/kugelgewindetriebe/berechnungsgrundlagen_kgt.pdf" target="_blank" rel="noopener noreferrer" class="external-link"&gt;manufacturer&amp;rsquo;s online documentation&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; is around &lt;code&gt;1700rpm&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;So, &lt;code&gt;17m/min&lt;/code&gt; remain. This is still fairly fast - I&amp;rsquo;ll be far away from full motor torque availability as motor torque reduces over speed. Let&amp;rsquo;s have a look at the stepper motor&amp;rsquo;s datasheet to make sure we&amp;rsquo;re safe.&lt;/p&gt;&#10;&lt;h4 id="verify-motor-torque-is-sufficient"&gt;Verify motor torque is sufficient&lt;/h4&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-03-03_torque_curve.jpg" alt="ESM.6088.42 Stepper torque curve"&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Datasheet: My stepper&amp;#39;s torque curve (Image courtesy of Sorotec GmbH)&lt;/span&gt;&lt;a&#10; href="https://www.sorotec.de/webshop/Datenblaetter/Schrittmotoren/ESM.6088.42/ESM.6088.42_V1.1.pdf"&#10; class="attr-link"&#10; aria-label="Attribution 1"&#10; &gt;&#10; &lt;sup class="attr-id"&gt;[1]&lt;/sup&gt;&#10; &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;The blue arrow indicates my most-often-to-be-used feed rate for roughing in wood where there&amp;rsquo;s plenty of torque available for large depth increments. According to the green arrow, I still have nearly 50% torque available during quick smoothing or while working with soft materials like foams. I decided to stay 30% below critical speed of the machine (limited by the ball screw) for my &lt;code&gt;G0&lt;/code&gt; commands that won&amp;rsquo;t run under spindle load. That&amp;rsquo;s where the yellow arrow is located: Maximum velocity is &lt;code&gt;12m/min = 200mm/sec&lt;/code&gt;. Here, only as much as &lt;code&gt;0.7Nm&lt;/code&gt; are available which is 1/6 of original torque.&lt;/p&gt;&#10;&lt;p&gt;This way, I have good confidence to enter velocity data for the axes X and Y. For Z with half the thread height, I divided it by 2.&lt;/p&gt;&#10;&lt;h3 id="acceleration"&gt;Axis acceleration&lt;/h3&gt;&#10;&lt;p&gt;Imagine you want to cut a 90° corner in your program with a feed rate of &lt;code&gt;12m/min&lt;/code&gt;. Not unlike you when riding a bike and taking a sharp turn, the machine is not able to brake to zero on one axis in no time and accelerate on the other one simultaneously. If the axes don&amp;rsquo;t wait for each other, we&amp;rsquo;ll cut the corner round. If we do wait, the feed rate is not constant anymore - a dilemma. That&amp;rsquo;s why we need to find appropriate acceleration values.&lt;/p&gt;&#10;&lt;p&gt;For your bike, decelerating &amp;ldquo;appropriately&amp;rdquo; might mean to not risk wheel lock or bicycle fork fracture but on the other hand still quickly enough to not &amp;ldquo;miss&amp;rdquo; the corner. Same thing for the CNC: We don&amp;rsquo;t want it to lose steps, resonate, block, jump or twist too much while maintaining a profound operating speed, high accuracy, and low machine wear.&lt;/p&gt;&#10;&lt;p&gt;Sounds impossible? That&amp;rsquo;s why we will spend some more time here.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2022-03-03_cnc_cornering.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Cornering: `vmax = 200mm/s`, `a=400mm/s^2`&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;Take the above example: I used a pen to visualize acceleration behavior of my CNC with some conservative acceleration values. For the last path, The CNC slows down to 0 so it can take the sharp corner. All the paths before it also won&amp;rsquo;t meet the targeted &lt;code&gt;200mm/sec&lt;/code&gt; but the larger the radius is, the quicker the machine ran.&lt;/p&gt;&#10;&lt;h4 id="motivation"&gt;Motivation&lt;/h4&gt;&#10;&lt;p&gt;&amp;ldquo;Why should Zerspanobert be accelerating quickly?&amp;rdquo; Here I have more answers. I&amp;rsquo;ll be doing a lot of short-line and 3D work. They often involve many lines of code and thus the CNC will have to change direction and feed rate all the time, maybe even use &lt;a href="https://www.mmsonline.com/articles/a-closer-look-at-look-ahead" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Look-Ahead Feed&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; or &lt;code&gt;G64&lt;/code&gt; commands.&lt;/p&gt;&#10;&lt;p&gt;I do also want to keep running through material with an almost constant feed rate to not generate &amp;ldquo;hot spots&amp;rdquo; where the machine dwells before accelerating into another direction.&#10;That&amp;rsquo;s why, with at low acceleration settings, these tasks will either take long or be less accurate than I&amp;rsquo;d like them to be as look-ahead will slur trajectories.&lt;/p&gt;&#10;&lt;p&gt;On the other hand, high accelerations will destabilize the machine and might lead to jerky behavior - but we&amp;rsquo;ll cover that later.&lt;/p&gt;&#10;&lt;h4 id="determine-maximum-acceleration-formula"&gt;Determine maximum acceleration formula&lt;/h4&gt;&#10;&lt;p&gt;As we did for velocity calculation already, let&amp;rsquo;s juggle some formulas to get a &lt;em&gt;do-not-exceed&lt;/em&gt; acceleration value. We need&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Newton%27s_laws_of_motion" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Newton&amp;rsquo;s second law&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; (axial movement): \(F_{ax} = m_pa\)&lt;/li&gt;&#10;&lt;li&gt;Inertia &lt;a href="https://en.wikipedia.org/wiki/Torque" target="_blank" rel="noopener noreferrer" class="external-link"&gt;torque&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; of the axis: \(M_a = \frac{a J}{P}\)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Moment_of_inertia" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Moment of inertia&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; of the axis (cylinder): \(J = m \frac{a}{2}r^2\)&lt;/li&gt;&#10;&lt;li&gt;Motor &lt;a href="https://tech.thk.com/de/products/pdf/de_b17_009.pdf" target="_blank" rel="noopener noreferrer" class="external-link"&gt;torque required&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; for linear movement: \(M_d =\frac{F_{ax} P}{2 \pi \eta} + M_a\)&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;To calculate the maximum acceleration, I combined these formulas to&lt;/p&gt;&#10;$$a = \frac{M_d}{\frac{m_p P}{2 \pi \eta} + \frac{J}{P}}$$&lt;p&gt;In words: The acceleration the machine can theoretically reach is the available motor torque \(M_d\) divided by the portal&amp;rsquo;s mass \(m_p\), weighed with ball screw specific parameters like thread width \(P\) and the overall motion system&amp;rsquo;s efficiency \(\eta\) plus the moment of inertia \(J\) the rotating axis components have, weighed again by thread width.&lt;/p&gt;&#10;&lt;h4 id="working-with-values"&gt;Working with values&lt;/h4&gt;&#10;&lt;p&gt;Let&amp;rsquo;s add some data - for my BasicLine 0607:&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;Parameter&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Symbol&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Value&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Unit&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;stepper torque @ vmax&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Md&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0.5&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Nm&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;portal mass&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;mp&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;40&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;kg&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;ball screw thread width&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;P&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0.01&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;m&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;ball screw radius&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;r&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0.008&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;m&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;axis&amp;rsquo; rotating mass&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;ma&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;kg&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;axis system efficiency&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;n&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;75&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;%&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;Where I didn&amp;rsquo;t know the exact values, I took worst-case data. For the stepper motor, assuming maximum velocity and lower voltage is worst-case because its torque will be much lower than at standstill at no load. I just guessed my portal weight and the rotating mass of my longest axis plus stepper&amp;rsquo;s core. Typical ball screw efficiency is at 90% but I had to take both bearings and losses of the pre-stressed linear guides into account. I conservatively estimated the axis system efficiency.&lt;/p&gt;&#10;&lt;p&gt;Ok, here&amp;rsquo;s what those values return in the formula as a maximum acceleration for my BasicLine CNC:&lt;/p&gt;&#10;$$a = 5.0m/s^2$$&lt;h4 id="what-this-value-means"&gt;What this value means&lt;/h4&gt;&#10;&lt;p&gt;Let&amp;rsquo;s see what time the machine would need to accelerate from 0 to top speed.&#10;Now good old law of &lt;a href="https://en.wikipedia.org/wiki/Acceleration" target="_blank" rel="noopener noreferrer" class="external-link"&gt;uniformly accelerated motion&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; comes in:&lt;/p&gt;&#10;$$v(t) = at \Rightarrow t = \frac{v_{max}}{a} = 40ms$$&lt;p&gt;How much distance do we cover to accelerate from 0 to full speed (\(s_0 = 0, v_0 = 0\))?&lt;/p&gt;&#10;$$s(t) = \frac{a}{2}t^2 + v_0t + s0 \Rightarrow s(t=40ms) = 4mm$$&lt;p&gt;What axial forces does the portal have to take during acceleration? \(F_{ax} = m_pa = 200N\)&lt;/p&gt;&#10;&lt;p&gt;The acceleration I calculated here is more than a magnitude larger than the manufacturer&amp;rsquo;s default setting &lt;code&gt;300mm/s^2&lt;/code&gt;. I wouldn&amp;rsquo;t have expected such a big difference.&lt;/p&gt;&#10;&lt;p&gt;🤔 &lt;em&gt;I must have overlooked something.&lt;/em&gt;&lt;/p&gt;&#10;&lt;h4 id="conclusion-a-matter-of-experience"&gt;Conclusion: A matter of experience&lt;/h4&gt;&#10;&lt;p&gt;The formulas neither take machine stiffness into account nor do they show how the machine would twist or jerk under dynamic load. What is Jerk, you ask? View this short &lt;a href="https://www.youtube.com/watch?v=9Z9Zws9K5kc" 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; for an explanation in CNC context. I didn&amp;rsquo;t find any point in my CNC software where I could set values for allowable Jerk, though.&lt;/p&gt;&#10;&lt;p&gt;Although I learned a lot about how math can help guide us here, it all boils down to experience again:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;How heavy and sturdy is the machine?&lt;/li&gt;&#10;&lt;li&gt;What kind of material am I working with: Is it forgiving or stiff?&lt;/li&gt;&#10;&lt;li&gt;Do I have complex, possibly 3D jobs to do?&lt;/li&gt;&#10;&lt;li&gt;Does my CNC live in a production environment where machine time is important?&lt;/li&gt;&#10;&lt;li&gt;How does the CNC sound and feel with high acceleration values?&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I entered a value of &lt;code&gt;800mm/s^2&lt;/code&gt; as acceleration for the X- and Y-axes. It seemed to work without issues whatsoever. After all, it remains below the calculated motion system&amp;rsquo;s capability by factor 5.&lt;/p&gt;&#10;&lt;h2 id="tryout"&gt;Tryout&lt;/h2&gt;&#10;&lt;p&gt;I know, this was a long read and much more an educated guess than conducting strict math 😅. Still, we&amp;rsquo;re now ready to home the machine the first time.&#10;Try it. After that, use the jog menu to go to Machine 0 on all axis. Does it stop before hitting the mechanical end?&lt;/p&gt;&#10;&lt;p&gt;Then use the continuous jog to move close to the other end of all axes. Carefully jog the last millimeters. Are measurements and settings all correct?&#10;If so, try the 100% speed modifier key (&lt;code&gt;Shift&lt;/code&gt; - on my keyboard). Does the CNC move rapidly and automatically stop at axis end without collision?&lt;/p&gt;&#10;&lt;p&gt;Congratulations!&lt;/p&gt;&#10;&lt;h2 id="max-motion-settings-calculator"&gt;Max motion settings calculator&lt;/h2&gt;&#10;&lt;p&gt;You bravely read the whole article. Kudos! Be rewarded with a little Excel tool: &lt;a href="https://blog.schallbert.de/assets/docs/cnc_kinematics_helper.xlsx"&gt;CNC kinematics helper&lt;/a&gt; you can download and use to work through some numbers for your machine. Needless to say that I cannot take any responsibility for the results you get when using that tool or any harm that wrong values can do to your machine or even to your health.&lt;/p&gt;&#10;&lt;aside class="update-box update-box--error" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ⛔&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Update of kinematics parameters&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2022-03-20T00:00:00Z"&gt;&#10; 2022-03-20&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; The value of &lt;code&gt;300mm/s&lt;/code&gt; was too quick and I would get software limit violation errors. Reduced to &lt;code&gt;240mm/s&lt;/code&gt;, my machine would vibrate a lot. So I just gently decreased maximum speed to &lt;code&gt;220mm/s&lt;/code&gt; which seems to be fine. I increased acceleration to &lt;code&gt;1200mm/s^2&lt;/code&gt;. The only issue with this new value is that for short movements, the machine is shaking just a bit more.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Update: Re-calculating parameter limits&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2022-04-03T00:00:00Z"&gt;&#10; 2022-04-03&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; The above calculation, while not wrong mathematically, is &lt;em&gt;misleading&lt;/em&gt;. It suggests that every axis could run with that speed which is just not the case. I have confirmed with &lt;a href="https://edingcnc.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;my machine&amp;rsquo;s control software supplier&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; that the 125kHz is the total max. command frequency of the system. If I try to squeeze in more steps, I&amp;rsquo;ll get an &lt;a href="https://blog.schallbert.de/en/projects/one-year-zerspanobert/#velocityerror"&gt;&amp;lsquo;Velocity was higher than max!&amp;rsquo;&lt;/a&gt; error. That&amp;rsquo;s why such high velocity value should &lt;em&gt;not&lt;/em&gt; be taken if the machines is planned to operate at full speed with all axes involved. I wrote a follow-up article and linked it &lt;a href="https://blog.schallbert.de/en/projects/one-year-zerspanobert/#velocityerror"&gt;here&lt;/a&gt; for your reference that uses values that work in any situation, even with all axes moving at full speed.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;h2 id="you-want-more"&gt;You want more?&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://blog.schallbert.de/en/edingcnc-config/"&gt;Machine software setup&lt;/a&gt;&lt;/p&gt;&#10;</description></item><item><title>CNC Part3 - Build</title><link>https://blog.schallbert.de/en/portal-milling-build/</link><pubDate>Tue, 01 Mar 2022</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/portal-milling-build/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-03-01-machine_complete-thumb.jpg"&#10; class="post-cover"&#10; alt="Completed build of Zerspanobert - Sorotec Basicline 0607"&#10; title="CNC Part3 - Build" /&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;iframe&#10; src="https://makertube.net/videos/embed/aAF2gaBewonh2uvu6mspze"&#10; title="&amp;amp;quot;Zerspanobert&amp;amp;quot; CNC assembly"&#10; loading="lazy"&#10; allow="autoplay; fullscreen; picture-in-picture"&#10; allowfullscreen&#10; style="width: 100%; height: auto; min-height: 315px; aspect-ratio: 16/9;"&#10; &gt;&lt;/iframe&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;&amp;amp;quot;Zerspanobert&amp;amp;quot; CNC assembly&lt;/span&gt;&lt;a href="https://makertube.net/videos/embed/aAF2gaBewonh2uvu6mspze" class="attr-link" target="_blank" rel="noopener noreferrer"&gt;↗&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;This is the 3rd part of my portal milling machine sequel. It concentrates on building the machine from an assembly kit.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/portal-milling/"&gt;&lt;em&gt;Part 1&lt;/em&gt;: Thoughts about CNCs in general and machine selection&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/cnc-electronics/"&gt;&lt;em&gt;Part 2&lt;/em&gt;: CNC Electronics build&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/portal-milling-setup/"&gt;&lt;em&gt;Part 4:&lt;/em&gt; Machine setup for first use&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="brand-sorotec-product-basicline-0607"&gt;Brand: Sorotec, Product: Basicline 0607&lt;/h2&gt;&#10;&lt;p&gt;My portal milling machine arrived as an all-inclusive assembly kit in a huge package on a small pallet. After inspecting its contents, I was pleased to find out that all major parts were packed so that I could start the build immediately.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-03-01-allparts.jpg" alt="parts included in the assembly kit"&gt;&lt;/figure&gt;&#10;&lt;h3 id="machine-facts"&gt;Machine facts&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Fully built, size is roughly 1m x 1m x 0.8m&lt;/li&gt;&#10;&lt;li&gt;Work area: ~ 630mm x 730mm x 135mm&lt;/li&gt;&#10;&lt;li&gt;Weight without spindle, mechatronics, and accessories: 52kg (source: manufacturer)&lt;/li&gt;&#10;&lt;li&gt;ball screws and linear guides on all axes&lt;/li&gt;&#10;&lt;li&gt;Steppers: 3Nm / 4.2A @48V&lt;/li&gt;&#10;&lt;li&gt;Speed &amp;amp; Acceleration config: 200mm/s, 400mm/s²&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="assembly"&gt;Assembly&lt;/h2&gt;&#10;&lt;p&gt;The portal milling machine&amp;rsquo;s mechatronics assembly took ~16h including stepper motor wiring, terminals, connectors, but excluding the switch box I wrote about &lt;a href="https://blog.schallbert.de/en/cnc-electronics/"&gt;here&lt;/a&gt; which I spent another 8h on.&lt;/p&gt;&#10;&lt;p&gt;The following sections each describe a construction stage, and also mention the things I struggled with.&lt;/p&gt;&#10;&lt;h3 id="1-preparing-the-ball-screw-bearings"&gt;1. Preparing the ball screw bearings&lt;/h3&gt;&#10;&lt;p&gt;A ball screw (&lt;a href="https://www.youtube.com/watch?v=W27pJXlO6ko" 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;) is a mechanical actor that converts rotary into linear motion. Instead of direct contact to a nut (like a normal screw), the nut houses a system of revolving balls that reduce friction between nut and screw to a minimum. On the CNC, they are used to move the portal with help of a stepper motor.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-03-01-ballscrews.jpg" alt="ballscrews with bearings"&gt;&lt;/figure&gt;&#10;&lt;p&gt;The ball screws need two bearings, a fixed (stepper motor side - black block on the images), and a loose one on the opposite side that just holds the screw in place. Mounting the loose-side ball bearing into its fitting proved to be more difficult to me than I&amp;rsquo;d have imagined. Im not experienced with metalworks at all, so I had a hard time hammering the bearing in as the fitting was so tight. I accidentally shaved off a bit of the aluminium fitting and the result was terrible.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2022-03-01_fitting-bad.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Fitting of ball bearing gone wrong&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;Luckily I had a 20mm ratchet nut that fits the outer ring of the ball bearing. After heating up the fitting a bit, I was able to use the nut, carefully hammering the bearing into the fitting with better results.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2022-03-01_fitting-ok.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Fitting of ball bearing after reinstall&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;Let&amp;rsquo;s hope the bearing didn&amp;rsquo;t get polluted too much with aluminium chips.&lt;/p&gt;&#10;&lt;h3 id="2-machine-frame"&gt;2. Machine frame&lt;/h3&gt;&#10;&lt;p&gt;The frame is made of &amp;ldquo;standard&amp;rdquo; aluminium profiles connected with brackets. Additional stiffening is provided by massive aluminium plates on front and rear of the frame. Linear guides are mounted to the sides of the frame which later keep the portal in place, allowing movement in &lt;code&gt;X-direction&lt;/code&gt; of travel.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-03-01-frame.jpg" alt="frame of Basicline 0607"&gt;&lt;/figure&gt;&#10;&lt;p&gt;It took me quite a long time to align the frame properly so that every angle would measure 90° and I&amp;rsquo;d still have some &amp;ldquo;air&amp;rdquo; at the right hand side for later arrangement as the gantry would dictate the exact frame width. Problem was that when tightening the screws from one side to the other, somehow the frame would always go out of square and I would have to loosen the screws a bit, then tighten up again, only to see that nothing much had improved.&lt;/p&gt;&#10;&lt;p&gt;After a couple of hours of cursing under my breath, I tried to tighten all screws just to 3Nm first, then to 6Nm, 9, etc. This worked very well so finally I reached the targeted 25Nm and was still happy with all the square angles 😅&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-03-01-xaxis.jpg" alt="Y-axis fully mounted, view from the rear"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Mounting front and rear plates, the ball screw for the Y axis, and the reference switch then proved to be quick and simple in comparison. The image above shows the fully mounted Y-axis. Linear bearings that will later carry the portal are already mounted on linear guides. Here&amp;rsquo;s how the trollies look like on the inside:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&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;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Linear bearing with revolving ball system&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h3 id="3-gantry"&gt;3. Gantry&lt;/h3&gt;&#10;&lt;p&gt;For this machine&amp;rsquo;s design, the portal moves in X-direction and provides a flange to the Z-axis that itself can be moved on the X-axis via a stepper motor on the portal&amp;rsquo;s left hand side.&lt;/p&gt;&#10;&lt;p&gt;First, the portal cheeks are mounted on the linear bearings of the Y-axis. Locator pins that the manufacurer designed were a great help to make sure that they are positioned correctly. Then, linear guides for the X-axis are attached to the crossbeam of the gantry which is then finally hung into the portal cheeks and secured with cast brackets. To keep the crossbar as close as possible to the cheeks before fastening, I used screw clamps.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-03-01-portal.jpg" alt="Gantry mounted, secured with clamps"&gt;&lt;/figure&gt;&#10;&lt;p&gt;This machine works with a single stepper motor for the Y-axis, not with two independent ones on each portal side that can be seen on other designs. To make the Y-axis&amp;rsquo;s ball screw move the portal, a girder is mounted underneath the portal cheeks that connects to the ball screw&amp;rsquo;s nut. It&amp;rsquo;s easy to spot in the video below. As the assembly was a bit heavy already and I had to access the underside of the frame, I used a wooden stand to carry one side of the frame while working underneath. This way I could avoid turning the whole machine upside-down.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2022-03-01_portal-move.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Easy portal moving by hand - without ball screw attached&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;After loosening some of the frame&amp;rsquo;s fasteners to readjust the air-gap with mounted portal, I made sure that the portal could move without difficulty. Then I tightened all relevant fasteners for portal and frame again, using the 3Nm-increment-technique I introduced before to avoid losing square.&lt;/p&gt;&#10;&lt;p&gt;Next step was to mount X-axis ball screws, linear bearings, and flange plate to the portal which luckily was easy enough to do.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-03-01-yaxis.jpg" alt="Y-axis with Z-flange before attaching to ball screw"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Finally, the X-axis reference switch could be added along with the motor socket.&lt;/p&gt;&#10;&lt;h3 id="4-z-axis"&gt;4. Z-Axis&lt;/h3&gt;&#10;&lt;p&gt;As the Z-axis is a component of its own, I assembled it aside the rest of the machine. Locators would simplify finding correct positions for the bearings, and a single linear guide would keep the Z-axis in place.&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;m pretty sure that this is fine for woodworks, but I don&amp;rsquo;t know if the stiffness provided by this Z-axis design is enough to routinely work with more challenging materials like Aluminium - for that, maybe a heavier alternative with two linear guides on the Z-axis should be used. Nevermind, I wanted a machine for woodworks and plastics - you get what you pay for.&lt;/p&gt;&#10;&lt;p&gt;It&amp;rsquo;s always nice to see when things really fall into place 🤗 - Look at how snugly the individual parts of this assembly kit fit together!&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2022-03-01_zaxis.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;I like the sound of nicely matching parts!&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;When component assembly was complete, I could mount the Z-axis to the gantry. Then I aligned the Z-axis with help of a dial gauge. What I observed: Even if I had invested much more time in further improving positioning errors, it is enough to push the Z-axis at the bottom end with my hand to increase deviations to above 0.01mm easily. Unless you have a very heavy (annealed steal) machine, don&amp;rsquo;t expect wonders.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-03-01-zaxis_alignment.jpg" alt="Z-axis alignment with dial gauge"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Assembling the stiffening cheeks to the Z-axis (image below) was more tricky than I anticipated. The hammer nuts were so small and the fasteners so short that there was not much room left for wiggling things in place - I had to align again and again to properly position them for sliding the reinforcement cheeks onto the assembly. When this was complete, it was an easy task to mount the Z-axis reference switch.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-03-01-zaxis_stiffening.jpg" alt="Z-axis stiffening cheeks, reference switch, X-axis motor mount"&gt;&lt;/figure&gt;&#10;&lt;h3 id="5-mechatronics"&gt;5. Mechatronics&lt;/h3&gt;&#10;&lt;p&gt;It&amp;rsquo;s time to mount the stepper motors I prepared &lt;a href="https://blog.schallbert.de/en/cnc-electronics/#stepper-motors"&gt;earlier&lt;/a&gt; . I lubricated the claw couplings between motor and ball screw with petrolatum and fully pushed it on the screw&amp;rsquo;s shaft before adding the motor. On the X-axis, fastening the setscrew was fiddly because the hex key has very little room to act on the motor side. If I had to build this machine another time, I&amp;rsquo;d do it like this to eliminate the issue:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Take the clutch apart&lt;/li&gt;&#10;&lt;li&gt;Fasten one clutch half to the ball screw&lt;/li&gt;&#10;&lt;li&gt;Add the other clutch half to the motor shaft, don&amp;rsquo;t tighten yet&lt;/li&gt;&#10;&lt;li&gt;Put the motor into its mount, only tighten so much you can still move it by hand&lt;/li&gt;&#10;&lt;li&gt;Push the motor&amp;rsquo;s clutch half into the screw&amp;rsquo;s&lt;/li&gt;&#10;&lt;li&gt;Take the motor off and fully tighten its clutch&lt;/li&gt;&#10;&lt;li&gt;Now mount the motor&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;As the motor wires are not very long, there is a small junction box located closely to each motor where both reference switch and motor power wires are connected to drag chain compatible control lines.&#10;Every stepper motor also gets a dedicated earth wire leading to the switchbox.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-03-01-xaxis_wiring.jpg" alt="Wiring X-axis stepper motor"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Once this was done for all three axes, I mounted the drag chains. Here, it&amp;rsquo;s a good idea to thoroughly select the mounting direction because the chains can be opened for maintenance in one direction only. Example: if you need to add or exchange cables later on, it&amp;rsquo;s frustrating to see that there&amp;rsquo;s no way of getting the screwdriver in place to unlock the chain links.&lt;/p&gt;&#10;&lt;p&gt;Also, please leave the control lines longer than needed and shorten them as a last step before soldering the connectors. For example, the required length of Z-axis wiring may come close to 5m when the switchbox cannot be placed immediately next to the machine.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-03-01-mechatronics.jpg" alt="All wiring done."&gt;&lt;/figure&gt;&#10;&lt;h3 id="6-lubrication"&gt;6. Lubrication&lt;/h3&gt;&#10;&lt;p&gt;Finally, I followed the (well-made) maintenance manual to get the machine properly lubricated.&lt;/p&gt;&#10;&lt;p&gt;Did I mention that I&amp;rsquo;m not a metalworks guy? Working with the grease gun proved to be pretty frustrating to me. At first, I didn&amp;rsquo;t manage to get anything out at all and finally, it wouldn&amp;rsquo;t stop. Now I&amp;rsquo;ve got a very-well greased working table but a dry machine.&lt;/p&gt;&#10;&lt;p&gt;And when is it enough grease? Are two shots into the grease nipple sufficient? I cannot even check whether anything actually entered the linear / ball screw bearings (they have a non-return valve), but the nipples are completely slurred from the outside.&lt;/p&gt;&#10;&lt;p&gt;Well, let&amp;rsquo;s assume everything is fine 😜&lt;/p&gt;&#10;&lt;h3 id="7-missing--bonus-parts"&gt;7. Missing / bonus parts&lt;/h3&gt;&#10;&lt;p&gt;As written above, all vital parts of the machine were packed Ok. But some small parts were missing for my machine configuration:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;18x 18mm M5 hex fasteners (linear guides X-axis)&lt;/li&gt;&#10;&lt;li&gt;6x 0.75mm cable shoes to get the earth wires mounted&lt;/li&gt;&#10;&lt;li&gt;4x 22mm M4 hex fasteners (stepper mount)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;But I also had some bonus parts&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;1m drag chain extra&lt;/li&gt;&#10;&lt;li&gt;4 M5 washers&lt;/li&gt;&#10;&lt;li&gt;9 M4x10 countersunk hex fasteners&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I&amp;rsquo;d recommend to add 100 zipties to the assembly kit so all wires can be kept nice and neat. Cable sleeves would be better still but likely come at higher cost.&lt;/p&gt;&#10;&lt;h3 id="8-critics"&gt;8. Critics&lt;/h3&gt;&#10;&lt;p&gt;I like the construction manual (Version 2.1.5) very much. It contains a parts overview and each step is explained in high detail. Also the build process is well thought-through and I as a non-professional mechanic didn&amp;rsquo;t have bigger troubles to correctly assemble the machine.&lt;/p&gt;&#10;&lt;p&gt;The machined parts have a high quality and are free of ridges or sharp corners. All necessary threads had been cut in advance - even the ones for the optional mechatronics kit - so that I didn&amp;rsquo;t have to buy tools or gain knowledge about how and where to mount them.&lt;/p&gt;&#10;&lt;p&gt;What I didn&amp;rsquo;t like is that some fasteners were missing holding up the completion for a weekend, and that the expected fastener count wasn&amp;rsquo;t printed in the manual so I only realized there are parts missing when it was already too late. I understand that this machine can be ordered in different sizes and configurations so it might be too much to expect a construction manual for each and every combination.&lt;/p&gt;&#10;&lt;p&gt;Some of the bigger aluminium parts arrived with scratches as the pallet somehow must have broken and torn through the packaging during transport. To me it shows that either the logistics company or the manufacturer didn&amp;rsquo;t pack the machine 100% safe or maybe underestimated its weight.&lt;/p&gt;&#10;&lt;p&gt;A last point about these grease nipples (still no metalworks guy): The one for the Y-axis cannot be turned anymore once the ball screw&amp;rsquo;s nut has been mounted to the lower gantry beam due to missing clearance. This is unfortunate when lubrication suddenly has to be done at a different angle than anticipated. Also, I&amp;rsquo;m pretty sure the lower grease nipple of the Z-axis points into the wrong direction in the manual.&lt;/p&gt;&#10;&lt;p&gt;I tried my best to find things to gripe about but came up with only so much as nitpicking. None of my critics limit the functionality of the machine and neither cost a considerable amount of time or money to fix. So far it looks like I bought a sturdy device that will help me do my prototyping woodworks more precisely and much more quickly than ever before.&lt;/p&gt;&#10;&lt;p&gt;This is &lt;strong&gt;Zerspanobert&lt;/strong&gt;!&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-03-01-machine_complete.jpg" alt="Sorotec BasicLine 0607 fully built"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;h3 id="9-lets-put-the-machine-into-operation"&gt;9. Let&amp;rsquo;s put the machine into operation!&lt;/h3&gt;&#10;&lt;p&gt;You want more? &lt;a href="https://blog.schallbert.de/en/portal-milling-setup/"&gt;Machine setup for first use&lt;/a&gt;&lt;/p&gt;&#10;</description></item><item><title>CNC Part2 - Electronics</title><link>https://blog.schallbert.de/en/cnc-electronics/</link><pubDate>Fri, 18 Feb 2022</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/cnc-electronics/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-02-18_switchbox-closed-thumb.jpg"&#10; class="post-cover"&#10; alt="switchbox of my CNC machine"&#10; title="CNC Part2 - Electronics" /&gt;&#10;&lt;h2 id="portal-machine-electronics"&gt;Portal machine electronics&lt;/h2&gt;&#10;&lt;p&gt;This is the second part of my &lt;a href="https://blog.schallbert.de/en/portal-milling/"&gt;Portal Milling Machine sequel&lt;/a&gt;. It is all about the electronics needed to control a CNC machine&amp;rsquo;s movement.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-02-18_switchbox.jpg" alt="Switch box, electronics fully assembled, opened"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;h3 id="ac--mains-components"&gt;AC / Mains components&lt;/h3&gt;&#10;&lt;p&gt;First, it houses main fuses for the whole machine control and a central power switch. It may contain mains-connected relays to control loads like spindles, cooling, dust collection. For these circuits, sub-fused power paths and outlet sockets might be available.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-02-18_sockets.jpg" alt="Switch box, outlet sockets"&gt;&lt;/figure&gt;&#10;Second, it has a power supply unit for the low voltage DC rail. Its voltage and power output will depend on your stepper motor load, control/breakout board voltage requirements, auxiliary low-voltage circuits it has to drive, and the region you&amp;rsquo;re living in because standards and regulations tend to differ.&lt;/p&gt;&#10;&lt;p&gt;Third, it may have additional signal relays to pair a variable frequency drive or similar.&lt;/p&gt;&#10;&lt;h3 id="vfd"&gt;Variable Frequency Drive&lt;/h3&gt;&#10;&lt;p&gt;A VFD is commonly known as a device that controls a 3-phase alternating current machine&amp;rsquo;s speed of rotation. Supplied with 1-phase or 3-phase AC, it buffers energy in a DC intermediate circuit from where it is then forwarded to a 3-phase &lt;a href="https://en.wikipedia.org/wiki/H-bridge" target="_blank" rel="noopener noreferrer" class="external-link"&gt;H-Bridge&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; that creates a 3-phase AC output with variable voltage and frequency.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-02-18_vfd-schematics.jpg" alt="Simplified schematics of a VFD&amp;#39;s power path"&gt;&lt;/figure&gt;&#10;As this 3-phase AC voltage is generated by transistors in switching mode which enormously decreases transformation losses in comparison to linear operation. The output voltage is quantified in time and magnitude though (&amp;ldquo;digital&amp;rdquo;), causing undesired circuit feedback and line noise both on the in- and output of the VFD.&lt;/p&gt;&#10;&lt;p&gt;Almost all &lt;a href="https://blog.schallbert.de/en/portal-milling/#spindle-power"&gt;professional CNC spindles&lt;/a&gt; are 3-phase AC asynchronous machines connected to a VFD.&lt;/p&gt;&#10;&lt;p&gt;VFDs often have a standardized 0&amp;hellip;10V analog input to control machine speed, enable inputs that are fed e.g. by a signal relay of the numerical control&amp;rsquo;s interface board, and an emergency halt or error signal output to the CNC interface.&lt;/p&gt;&#10;&lt;p&gt;As of now (2022-Feb), I don&amp;rsquo;t own a VFD. So I don&amp;rsquo;t know any details about tuning, parameter setting, and customization for a certain spindle to write about.&lt;/p&gt;&#10;&lt;h3 id="elv--dc-components"&gt;ELV / DC components&lt;/h3&gt;&#10;&lt;p&gt;The typical &amp;ldquo;small&amp;rdquo; CNC&amp;rsquo;s stepper motors are driven in the PELV range (&lt;a href="https://en.wikipedia.org/wiki/Extra-low_voltage" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Protected Extra Low Voltage&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;) with direct current (DC) and below &lt;code&gt;60V&lt;/code&gt; so no additional touch protection is needed. The &amp;ldquo;P&amp;rdquo; in PELV stands for &amp;ldquo;protected&amp;rdquo; which means that the devices are connected to grounding wires. This way, a short to ground will safely turn the circuits off, blowing the fuses.&lt;/p&gt;&#10;&lt;p&gt;The stepper motor drivers, one each per axis, are also placed within the box. They are typically SELV (Safety Extra Low Voltage) so that they don&amp;rsquo;t need an extra grounding.&lt;/p&gt;&#10;&lt;p&gt;Then there&amp;rsquo;s a signal board, breakout board, or relay board that forwards stepper signal trains to the drivers, controls auxiliary relays, reads reference switch and emergency stop inputs, and output signals for spindle, cooling, dust collection, and/or auxiliary controls.&lt;/p&gt;&#10;&lt;p&gt;A fan keeps temperatures within the switch box low. Maybe there also is an interface board mounted in the switch box that is controlled directly via the CNC software, e.g. through USB or ethernet. It translates and routes information to the CNC&amp;rsquo;s individual components and back to the program, e.g. for reference switches or emergency stop.&lt;/p&gt;&#10;&lt;h3 id="stepper-motor-drivers-in-detail"&gt;Stepper motor drivers in detail&lt;/h3&gt;&#10;&lt;p&gt;The stepper driver interface is quite simple. It takes TTL level of &lt;code&gt;0V (low)&lt;/code&gt; and &lt;code&gt;5V (high)&lt;/code&gt; and is defined by only three inputs:&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;Signal name&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Description&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;Enable (EN)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Enables power to the drives. E.g. allows active position hold&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;Direction (DIR)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Select direction of motor rotation&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;Pulse (PUL)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Makes the motor turn a (micro)step&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;Interestingly, the same three input signals are needed to control a floppy disk drive&amp;rsquo;s reading head. I did this in one of my earlier projects, a floppy-bass-organ based on the software project &lt;a href="https://blog.arduino.cc/2012/05/29/music-floppy-moppy/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;moppy&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-02-18_drivers.jpg" alt="Stepper drivers, top view"&gt;&lt;/figure&gt;&#10;Internally, the driver interprets input signals and outputs higher voltage, high current pulses that are suited to drive the stepper motor. Two H-bridge channels - circuits with four switching transistors to control direction and magnitude of current flow through the motor&amp;rsquo;s coils - called &lt;code&gt;A&lt;/code&gt; and &lt;code&gt;B&lt;/code&gt; provide energy to the pole pairs of the stepper motor.&lt;/p&gt;&#10;&lt;p&gt;You can find more detailed information on stepper motor drives, microstepping, and other aspects e.g. &lt;a href="https://blog.poscope.com/stepper-motor-driver/#What_is_a_stepper_motor_driver" target="_blank" rel="noopener noreferrer" class="external-link"&gt;here&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; or on &lt;a href="http://www.schrittmotor-blog.de/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;this blog&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; (in German language).&lt;/p&gt;&#10;&lt;h3 id="stepper-motors"&gt;Stepper motors&lt;/h3&gt;&#10;&lt;p&gt;Stepper motors - like 3-phase AC or brushless DC motors - are electronically commutated. So direction and magnitude of current pulses have to be provided to the motor to make it turn continually. &lt;a href="http://stepcontrol.com/pdf/step101.pdf" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Most stepper motors nowadays are hybrid motors&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; that combine a multi-toothed rotor, split into two halves that are shifted to each other by half a tooth, each controlled by one coil path.&#10;This kind of motor has four wires at least, two for coil &lt;code&gt;A&lt;/code&gt; and two for coil &lt;code&gt;B&lt;/code&gt;.&#10;Unifilar hybrid motors even have eight leads that again split the coils into two parts - this way, they can be connected in series, in parallel, and also unipolar so each of the four coils can be controlled individually.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-02-18_stepper-cable.jpg" alt="Stepper cables, twisted"&gt;&lt;/figure&gt;&#10;The most common way to connect a stepper motor to a CNC stepper driver is the &amp;ldquo;bipolar parallel circuit&amp;rdquo;: Here, the two coils of a pole pair are connected in parallel with the same orientation. The two wires from this parallel circuit are then connected to the driver&amp;rsquo;s &lt;code&gt;A+&lt;/code&gt; and &lt;code&gt;A-&lt;/code&gt; outputs. This wiring is repeated for channel &lt;code&gt;B&lt;/code&gt;.&#10;I twisted the wire pairs that belong together with a power drill because I was too lazy to do this manually.&lt;/p&gt;&#10;&lt;p&gt;Unlike 3-phase AC or brushless DC machines, priority for stepper motor design does not lie on highest possible energy efficiency, low noise emissions, high rotation speed or thermal management for continuous operation, but on torque for low speeds, high clamping forces to securely hold a position, and smooth operation at low speeds. Stepper motors are built for high acceleration and shall always follow the speed of pulses as the machine&amp;rsquo;s positioning depends on that.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-02-18_stepper-connect.jpg" alt="Stepper, connected to a driver in bipolar parallel configuration"&gt;&lt;/figure&gt;&#10;Unlike servo motors, where a closed-loop control guarantees exact positioning by measuring the shaft&amp;rsquo;s position, the stepper motor&amp;rsquo;s position is assumed. The steps are counted and multiplied with the known angle the motor turns per step. This works very well as long as the motor&amp;rsquo;s maximum load is not exceeded in which case steps are lost, creating position errors.&lt;/p&gt;&#10;&lt;h2 id="the-build"&gt;The build&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2022-02-18_switchbox-build.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Switchbox build in 9 seconds&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;The build of the switch-box and wiring assembly kit that I bought took just about 8h of my time. It proved crucial to thoroughly read and understand all of the circuit plans and step-by-step instructions provided in advance to avoid mistakes that could otherwise cost hours to fix.&lt;/p&gt;&#10;&lt;p&gt;About the kit I like that the components are electrically matched and all necessary wires, sockets (apart from an ethernet socket), and the documentations were complete. I only missed cable straps and some lugs to simplify cable management and connections to the housing. The housing is very robust and all sockets fit in their places nicely.&lt;/p&gt;&#10;&lt;p&gt;Most painful when building where the solder joints of the stepper motor/D-sub connectors and their insulation. It was also not very easy to mount the numerical controller board in the switchbox.&lt;/p&gt;&#10;&lt;p&gt;I was really excited when I first turned the switchbox on. Was I relieved when the fan started working and all sorts of LEDs lit up to indicate a working system.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2022-02-18_switchbox-test.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Switchbox dry run: Checking emergency off&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h3 id="safety-notice"&gt;Safety notice&lt;/h3&gt;&#10;&lt;p&gt;No, an opened mains powered device shouldn&amp;rsquo;t be turned on unless you know what you&amp;rsquo;re doing. Yes, I have a residual current device installed in my tech shack to protect myself from hazardous voltages, and yes, I had all fuses double-checked, did quite some visual inspection and performed continuity measurements on every single connection to exclude miswirings. Luckily, the control box passed the initial function test. Both the emergency signal state was correctly interpreted and also the CNC board&amp;rsquo;s LED flashed with the expected &lt;code&gt;1Hz&lt;/code&gt; rate.&lt;/p&gt;&#10;&lt;h3 id="building-a-mini-control--series-c1"&gt;Building a &amp;ldquo;Mini Control / Series C1&amp;rdquo;&lt;/h3&gt;&#10;&lt;p&gt;For the Maker Faire Ruhr 2024, where I was one of the exhibitors, I built another machine control box: The simpler &amp;ldquo;Mini Control / Series C1&amp;rdquo;. I have embedded a time-lapse video of it here:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;iframe&#10; src="https://makertube.net/videos/embed/hvZabGKMgP6QWeD5a8wgKg"&#10; title="CNC electronics assembly"&#10; loading="lazy"&#10; allow="autoplay; fullscreen; picture-in-picture"&#10; allowfullscreen&#10; style="width: 100%; height: auto; min-height: 315px; aspect-ratio: 16/9;"&#10; &gt;&lt;/iframe&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;CNC electronics assembly&lt;/span&gt;&lt;a href="https://makertube.net/videos/embed/hvZabGKMgP6QWeD5a8wgKg" class="attr-link" target="_blank" rel="noopener noreferrer"&gt;↗&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;Advantages:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Quick assembly&lt;/li&gt;&#10;&lt;li&gt;No soldering&lt;/li&gt;&#10;&lt;li&gt;Many snap-in parts&lt;/li&gt;&#10;&lt;li&gt;Small housing&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Disadvantages:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Connection to the machine is via screw terminals; Separation from the machine not possible&lt;/li&gt;&#10;&lt;li&gt;Loud fan, active even when idling (but can be solved with &amp;ldquo;Schallbert&amp;rsquo;s Fanhack&amp;rdquo;)&lt;/li&gt;&#10;&lt;li&gt;Several external power supplies (stepper motor supply, 24V supply, spindle switch box)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I did a lot with the associated small CNC milling machine: &lt;a href="https://blog.schallbert.de/en/cnc-tuning-kinematics/"&gt;Tuning&lt;/a&gt; for shorter production times, a specialization for engravings and various projects such as stamps, dithered images and multi-colored &lt;a href="https://blog.schallbert.de/en/engrave-multicolor/"&gt;carves&lt;/a&gt;.&lt;/p&gt;&#10;</description></item><item><title>CNC Part1 - Portal milling machine</title><link>https://blog.schallbert.de/en/portal-milling/</link><pubDate>Sat, 12 Feb 2022</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/portal-milling/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-02-12_portalmillingmachine-thumb.jpg"&#10; class="post-cover"&#10; alt="My portal milling machine in mid-construction"&#10; title="CNC Part1 - Portal milling machine" /&gt;&#10;&lt;h2 id="cnc-portal-milling-machines---a-beginners-view"&gt;CNC Portal Milling Machines - a beginner&amp;rsquo;s view&lt;/h2&gt;&#10;&lt;p&gt;My first computerized milling machine will be delivered very soon, in form of a lot of packages with even more parts inside that require assembling.&#10;I ordered it to speed up my &lt;a href="https://blog.schallbert.de/en/projects/aa_alpha_1_1/"&gt;AnywhereAmps&lt;/a&gt; prototyping process. And of course to realize some other hardware projects that I had in mind, but was never able to build. Yet.&lt;/p&gt;&#10;&lt;h3 id="cnc"&gt;Wait, what?&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-02-12_gcode.jpg" alt="G-code example"&gt;&lt;/figure&gt;&#10;Oh, Ok. CNC means &lt;a href="https://en.wikipedia.org/wiki/Numerical_control" target="_blank" rel="noopener noreferrer" class="external-link"&gt;computer numerical control&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and describes how the tool is controlled - not by hand but by code.&lt;/p&gt;&#10;&lt;p&gt;This is called &lt;a href="https://en.wikipedia.org/wiki/G-code" target="_blank" rel="noopener noreferrer" class="external-link"&gt;G-Code&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, a widely used language to tell CNC machines like my portal milling machine what to do.&lt;/p&gt;&#10;&lt;p&gt;A portal milling machine is like a router, just not hand-held. It is mounted on an apparature that can move the router in all three axes X, Y, Z with the help of &lt;a href="https://en.wikipedia.org/wiki/Stepper_motor" target="_blank" rel="noopener noreferrer" class="external-link"&gt;stepper motors&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; or &lt;a href="https://en.wikipedia.org/wiki/Servomotor" target="_blank" rel="noopener noreferrer" class="external-link"&gt;servo motors&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; so that a computer can control its movement and cut any path you like. Its way of operating is similar to a &lt;a href="https://en.wikipedia.org/wiki/3D_printing#Processes_and_printers" target="_blank" rel="noopener noreferrer" class="external-link"&gt;3D printer&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, although the mechanics of the two have completely different requirements - a portal milling machine has to be constructed much more rugged, heavy, and stiff than a 3D-printer as it has to take all forces created by cutting through material.&lt;/p&gt;&#10;&lt;p&gt;Of course, CNC machines are not limited to 3D printers and routers. There are CNC &lt;a href="https://en.wikipedia.org/wiki/Lathe" target="_blank" rel="noopener noreferrer" class="external-link"&gt;lathes&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://en.wikipedia.org/wiki/Plotter" target="_blank" rel="noopener noreferrer" class="external-link"&gt;plotters&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://en.wikipedia.org/wiki/Laser_cutting" target="_blank" rel="noopener noreferrer" class="external-link"&gt;laser cutters&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://en.wikipedia.org/wiki/Water_jet_cutter" target="_blank" rel="noopener noreferrer" class="external-link"&gt;water jet cutters&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and many more. Strictly speaking even 6+ axis industry robots are CNC machines.&lt;/p&gt;&#10;&lt;h2 id="questions"&gt;Questions&lt;/h2&gt;&#10;&lt;p&gt;These are some of the questions I had before actually buying this pretty expensive piece of technology. Some of them are easy to answer in just a few lines, but some likely take a whole article on their own.&lt;/p&gt;&#10;&lt;h3 id="cnc-router-decision"&gt;CNC router decision&lt;/h3&gt;&#10;&lt;h4 id="does-it-really-speed-up-my-prototyping-process"&gt;Does it really speed up my prototyping process?&lt;/h4&gt;&#10;&lt;p&gt;I don&amp;rsquo;t know yet, but I hope so. Consider this: Woodworks for a single prototype take roughly 8h of my time. Most time consuming are preparations for cutting, measurements, and tracing. What takes more than an hour of manufacturing time alone is AnywhereAmps Alpha&amp;rsquo;s speaker adapter with its curved surface.&lt;/p&gt;&#10;&lt;p&gt;With a CNC portal milling machine, I&amp;rsquo;m hoping to become more like a biscuit maker: Nesting all parts for AnywhereAmps (apart from the cylindrical housing) within a single piece of plywood, having the machine do the work and, after a while, just minimal finalizing work for cutting and sanding the parts.&lt;/p&gt;&#10;&lt;p&gt;Plus, if I want another prototype or have other speaker diameters: Adjust some parameters within my CAD tool, rebuild tool paths, send to the machine&amp;rsquo;s computer, clamp a fresh piece of plywood and hit &amp;ldquo;run job&amp;rdquo;. Done (in the ideal world).&lt;/p&gt;&#10;&lt;p&gt;On the other hand, the software tools process takes its time, too. Find my introduction article on &lt;a href="https://blog.schallbert.de/en/freecad-get-started/"&gt;FreeCAD linked here&lt;/a&gt; as an example.&lt;/p&gt;&#10;&lt;h4 id="spend-more-money-on-portal-hardware-or-rather-on-spindle-drive"&gt;Spend more money on portal hardware or rather on spindle drive?&lt;/h4&gt;&#10;&lt;p&gt;This is a question to the experts and I&amp;rsquo;ll only be able to answer it myself after a while of owning a CNC. I chose to spend more money on the portal and less on the spindle for a couple of gut-feeling reasons:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;The portal is my machine, the spindle just a component of it.&lt;/li&gt;&#10;&lt;li&gt;The spindle is cheaper, more easy to exchange, and by far less work to mount than the portal so I&amp;rsquo;d want the portal to be a constant&lt;/li&gt;&#10;&lt;li&gt;If I&amp;rsquo;m limited by the capabilities of my portal milling machine, I&amp;rsquo;d have to first figure out why, and then fiddle with individual components to improve. If the spindle is the limiting factor, I&amp;rsquo;ll just buy a more powerful one.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h4 id="spindle-power"&gt;How much spindle power?&lt;/h4&gt;&#10;&lt;p&gt;That&amp;rsquo;s my first hard one. I&amp;rsquo;m almost certain that for my woodworking stuff, I don&amp;rsquo;t need a 2.2kW monster. To get decent feed rates and to maintain enough headroom for density inconsistencies like branch forks, it might be a good idea to orientate in the hand-router wattage segment between 700W and 1200W which always served me well. I chose a 1000W &lt;a href="https://en.wikipedia.org/wiki/Universal_motor#cite_note-tm-6" target="_blank" rel="noopener noreferrer" class="external-link"&gt;universal motor&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; machine.&lt;/p&gt;&#10;&lt;p&gt;In contrast to their three-phase induction machine counterparts often called &amp;ldquo;HF-Spindles&amp;rdquo; (HF = High Frequency), they don&amp;rsquo;t require a &lt;a href="https://blog.schallbert.de/en/cnc-electronics/#vfd"&gt;variable frequency drive&lt;/a&gt; and external cooling as their fan is bolt-on their shaft. On the other hand, they tend to be louder (the fan is not independent off the motor&amp;rsquo;s RPM) and less efficient (fan losses, commutation losses, copper losses, magnetization losses etc.) I found only rough figures on the internet when it comes to efficiency, so let&amp;rsquo;s assume:&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;1000W&lt;/code&gt; universal motor drive &lt;code&gt;*30%&lt;/code&gt; total efficiency incl. Electronics &amp;amp; fan: &lt;code&gt;300W&lt;/code&gt; @ motor shaft&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;700W&lt;/code&gt; &lt;a href="https://www.scribd.com/document/776268309/2-4-6polig-e-M-asynchron-IP44-IP56" target="_blank" rel="noopener noreferrer" class="external-link"&gt;asynchronous drive&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; &lt;code&gt;*85%&lt;/code&gt; efficiency &lt;code&gt;*90%&lt;/code&gt; &lt;a href="http://www.variablefrequencydrive.org/vfd-efficiency" target="_blank" rel="noopener noreferrer" class="external-link"&gt;VFD efficiency&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; &lt;code&gt;*97%&lt;/code&gt; external cooling: &lt;code&gt;520W&lt;/code&gt; @ motor shaft&lt;/p&gt;&#10;&lt;p&gt;This would be a huge difference still, so an upgrade at a later point in time might be worth after all. Plus, lower wattage ratings will suffice for a HF-spindle compared to a conventional drive.&lt;/p&gt;&#10;&lt;h4 id="tool-changer"&gt;Automatic Tool Changer - yes or no?&lt;/h4&gt;&#10;&lt;p&gt;It depends on what I need, I guess. I&amp;rsquo;m buying a simple AC hand-router with digital speed control as a spindle first. A &amp;ldquo;proper&amp;rdquo; tool changer - proper meaning that I can also mount tool shafts of 10mm and bigger, have balanced collets, 3-Phase drive with stand-alone cooling and variable frequency drive - would cost ~10 times as much. Plus, each ER20 collet chuck for the tool changer easily comes at 1/2 price of the simple AC hand-router cost. Of which I&amp;rsquo;d require more than one if I want the tool changer to make sense. And I&amp;rsquo;d need a compressed air equipment with control box etc. The total of this would easily outweigh the portal milling machine&amp;rsquo;s cost!&lt;/p&gt;&#10;&lt;p&gt;Conclusion: If I&amp;rsquo;ll be using the CNC very often in the future and if my projects absolutely require frequent tool changes for roughing, smoothing, chamfers, 3D etc., then I might consider this expensive step. Maybe. In the distant future.&lt;/p&gt;&#10;&lt;h4 id="which-size-is-best"&gt;Which size is best?&lt;/h4&gt;&#10;&lt;p&gt;Ideally, for working with plywood sheets, it could load a full standard sheet of &lt;code&gt;1250mm x 2500mm&lt;/code&gt; or a fraction of it, e.g. &lt;code&gt;610mm x 1250mm&lt;/code&gt;. But even the smaller ones wouldn&amp;rsquo;t fit my tiny flat&amp;rsquo;s cellar. So I chose one with &lt;code&gt;840mm x 740mm&lt;/code&gt; clamping area.&lt;/p&gt;&#10;&lt;p&gt;For your choice: Check out if you&amp;rsquo;re constrained in space. If not, then thoroughly make up your mind about what you want to do with your machine. The machine size will fall into place automatically when you know your primary use cases.&lt;/p&gt;&#10;&lt;h4 id="plug-and-play-or-an-assembly-kit"&gt;plug-and-play or an assembly kit?&lt;/h4&gt;&#10;&lt;p&gt;This depends on multiple aspects: Your budget, time you&amp;rsquo;d like to invest, and of course, if you&amp;rsquo;re at least a bit talented with screwdrivers, nut runners, cable strippers, and the soldering iron.&#10;It is also a matter of if you&amp;rsquo;d like to or need to know every bolt of your machine.&lt;/p&gt;&#10;&lt;h4 id="ball-screws-for-moving-the-axes"&gt;ball screws for moving the axes?&lt;/h4&gt;&#10;&lt;p&gt;Do you want rapid moves, high repeatability due to reduced or eliminated play, or higher feed rates? Do you want climb milling for improved surface quality and longer tool life? If any of these is a definitive yes, then you&amp;rsquo;ll have to spend the extra money for ball screw axis drives.&lt;/p&gt;&#10;&lt;h4 id="additional-versatility"&gt;additional versatility?&lt;/h4&gt;&#10;&lt;p&gt;As described in the &lt;a href="https://blog.schallbert.de/en/portal-milling/#cnc"&gt;wait, what?&lt;/a&gt; section, the portal itself would also be suitable for other purposes. There are dragknifes for plotting, 3D print heads, and even engraving lasers available that could be fit to the portal if needed.&lt;/p&gt;&#10;&lt;p&gt;To prepare for this, a spindle that fits the euro-standard &lt;code&gt;43mm&lt;/code&gt; diameter neck holder possibly is the best choice because most other tools are available for this fit as well, making a conversion more easy.&lt;/p&gt;&#10;&lt;h3 id="general"&gt;General&lt;/h3&gt;&#10;&lt;h4 id="does-it-create-lots-of-dust"&gt;Does it create lots of dust?&lt;/h4&gt;&#10;&lt;p&gt;Yes. Get a proper dust collection. I&amp;rsquo;d recommend a cyclone system or one of these huge textile bag collection thingies that are usually used with table saws. If you&amp;rsquo;re using your machine regularly, even a shop vacuum alone might not have the holding capacity you require.&lt;/p&gt;&#10;&lt;p&gt;You should also ground it well (e.g. by connecting its ground wire(s) to your switchbox&amp;rsquo;s ground point) and buy antistatic vacuum hoses which you can connect to the vacuum&amp;rsquo;s or the dust collector&amp;rsquo;s earth.&#10;This way, you avoid getting &lt;em&gt;bzzz&lt;/em&gt;&amp;rsquo;ed everytime you touch vacuum-related things plus you can exclude one hard to spot reason for your machine going mad, e.g. sudden software freezes, control randomly detecting Emergency Stop events, phantom reference switch clicking or even stepper pulse hiccups. There are many appearances in diverse forums where this was or could have been the culprit, e.g. &lt;a href="https://cambamcnc.com/forum/index.php?topic=4524.0" target="_blank" rel="noopener noreferrer" class="external-link"&gt;here&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h4 id="is-it-loud"&gt;Is it loud?&lt;/h4&gt;&#10;&lt;p&gt;Yes. The sound level is comparable to a hand router. It is loud in idle already, and it gets worse when you plunge through material. So it might be a good idea to either buy the quieter but more expensive high-frequency spindles with external cooling. In addition, your machine should (&lt;a href="https://eur-lex.europa.eu/legal-content/DE/TXT/PDF/?uri=CELEX:02006L0042-20091215&amp;amp;rid=1" target="_blank" rel="noopener noreferrer" class="external-link"&gt;and according to your local regulations even might have to&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;) get an enclosure for safety reasons, but also for additional noise reduction. It will also help reduce fine particle dust.&lt;/p&gt;&#10;&lt;p&gt;But sound also heavily depends on the bit you&amp;rsquo;re using. In my experience, large diameter, 10mm shaft, short bits &lt;a href="https://blog.schallbert.de/en/my-endmill-screams/"&gt;sound more pleasant&lt;/a&gt; and are maybe even less noisy than smaller diameter or long cutters.&lt;/p&gt;&#10;&lt;h4 id="what-is-the-workflow"&gt;What is the workflow?&lt;/h4&gt;&#10;&lt;p&gt;Here&amp;rsquo;s a simplified image of the workflow with a CNC machine.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2022-02-12_cncworkflow.jpg" alt="Image: CNC workflow in a nutshell"&gt;&lt;/figure&gt;&#10;Similar to hand-crafting, it all starts with an idea. Parts drawings and dimensioning are made in a Computer Aided Design tool instead of on a blank sheet of paper. The drawing or 3D model file is then further proessed in a Computer Aided Manufacturing tool, where suitable router tools are selected, the milling strategies are defined, and the toolpaths generated. The resulting G-Code file is then sent to the actual CNC software which controls the stepper motor drivers. They are sending current pulses to the stepper motors which then mechanically move the machine to create the part.&lt;/p&gt;&#10;&lt;h3 id="operational"&gt;Operational&lt;/h3&gt;&#10;&lt;h4 id="milling-style"&gt;conventional or climb milling?&lt;/h4&gt;&#10;&lt;p&gt;This is a choice you make in your CAM tool.&lt;/p&gt;&#10;&lt;p&gt;Conventional milling = the router bit &amp;ldquo;carves&amp;rdquo; the material, its cutting edges rotate against the direction of material transport. Table saws e.g. always use this to avoid kick backs.&#10;Climb milling = the router bit &amp;ldquo;eats&amp;rdquo; into the material, its cutting edges rotate with the direction of transport. The cutter tends to pull itself into the material.&#10;More reference e.g. on &lt;a href="https://www.harveyperformance.com/in-the-loupe/conventional-vs-climb-milling/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;harveryperformance&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; or &lt;a href="https://blog.tormach.com/climb-milling-versus-conventional-milling-sneaky-cnc-tricks" target="_blank" rel="noopener noreferrer" class="external-link"&gt;tormach&amp;rsquo;s blog&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Which of the two you should choose depends on your machine&amp;rsquo;s stiffness and weight, your stepper motor&amp;rsquo;s holding capabilities, the power of your stepper drivers, and the material you&amp;rsquo;re working with. If you have a stiff, heavy, powerful machine (with ball screws) and work with materials on the soft side, use climb milling. Watch your machine closely to see if all feels and sounds (!) smooth. If in doubt, use conventional milling. For hand-operated routers, always use conventional milling!&lt;/p&gt;&#10;&lt;h4 id="how-to-create-easy-and-quick-workholding"&gt;How to create easy and quick workholding?&lt;/h4&gt;&#10;&lt;p&gt;This again depends on your stock:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Big Sheets: Vaccuum table or guard fence with clamps that pushes your sheet against the fence&lt;/li&gt;&#10;&lt;li&gt;Small sheets: Clamps or bolt-down to table&lt;/li&gt;&#10;&lt;li&gt;Small blocks: Vise&lt;/li&gt;&#10;&lt;li&gt;Irregular shapes: Clamps that force the stock down to the working table&lt;/li&gt;&#10;&lt;li&gt;Dual side milling: Fixtures you should mill and glue the stock to for this specific project&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Hmm, the &lt;a href="https://blog.schallbert.de/en/projects/cnc_spoilboard/"&gt;spoilboard with guard fence&lt;/a&gt; will likely be one of my first projects with the new machine.&lt;/p&gt;&#10;&lt;h4 id="which-bits--end-mills-to-start-with"&gt;Which bits / end mills to start with?&lt;/h4&gt;&#10;&lt;p&gt;I&amp;rsquo;d recommend the following general mills for a start. They should of course match your material in their making, spiral type, flute count etc.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Classic 6mm or 1/4&amp;quot; upcut end mill&lt;/li&gt;&#10;&lt;li&gt;2mm upcut end mill for fine stuff or small holes&lt;/li&gt;&#10;&lt;li&gt;90° engraving mill for chamfers and engravings&lt;/li&gt;&#10;&lt;li&gt;Face mill of your choice for huge surface milling&lt;/li&gt;&#10;&lt;li&gt;ball-nose mill for 3D smoothing&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h4 id="my-focus-is-plywood-what-are-suitable-feeds-and-speeds"&gt;My focus is plywood, what are suitable feeds and speeds?&lt;/h4&gt;&#10;&lt;p&gt;Birch plywood is hard but forgiving. Suitable values always take into account machine stability, power and spindle performance.&lt;/p&gt;&#10;&lt;p&gt;Remember: You want your bit to cut chips, not create dust. That&amp;rsquo;s why you should avoid low feed rates at high spindle RPM. For starters in wood: use half of your tool diameter as depth per pass, calculate the spindle RPM using feeds &amp;amp; speeds formula or a calculator from the internet, and try these out. Look at the chips. Listen to the machine while working. Does it sound &amp;ldquo;good&amp;rdquo;? Increase depth per pass or feed rate gradually, stop at where either your spindle sounds stressed, your finish deteriorates, or you feel less confident with your machine. Turn back again just a notch, and that might be your ideal feed rate &amp;amp; depth per pass settings for material and mill you&amp;rsquo;re currently using.&lt;/p&gt;&#10;&lt;p&gt;For my machine, I confidently chose &lt;code&gt;F 3500mm/min = 138in/m&lt;/code&gt; at &lt;code&gt;S 24000 RPM&lt;/code&gt; and a depth increment of the mill&amp;rsquo;s diameter &lt;code&gt;Z 6mm&lt;/code&gt;, and lane-by-lane feed of &lt;code&gt;90%&lt;/code&gt;. For your reference, I used the websites &lt;a href="https://www.cnccookbook.com/feeds-speeds-cnc-wood-cutting/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;cnccookbook&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://www.cncrechner.de/rechner/vorschub/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;cnc-rechner&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, and tool manufacturer&amp;rsquo;s websites like &lt;a href="https://webseite.sorotec.de/download/fraesparameter/schnittwerte.pdf" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Sorotec&amp;rsquo;s&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; to calculate mine.&lt;/p&gt;&#10;&lt;h2 id="continue-reading"&gt;Continue reading?&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://blog.schallbert.de/en/portal-milling-build/"&gt;CNC machine: the build&lt;/a&gt;&#10;&lt;a href="https://blog.schallbert.de/en/cnc-electronics/"&gt;CNC machine: electronics&lt;/a&gt;&lt;/p&gt;&#10;</description></item><item><title>Inrush Current Limiter - Part2</title><link>https://blog.schallbert.de/en/inrushlimiter-circuits/</link><pubDate>Thu, 10 Feb 2022</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/inrushlimiter-circuits/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/inrushlimiter-thumb.jpg"&#10; class="post-cover"&#10; alt="Inrush limiter circuit variant"&#10; title="Inrush Current Limiter - Part2" /&gt;&#10;&lt;h2 id="contents"&gt;Contents&lt;/h2&gt;&#10;&lt;p&gt;In this article, I will concentrate on two ways to create a solid-state inrush current limiter circuit: One copies the relay-approach from &lt;a href="https://blog.schallbert.de/en/knowledge-inrushlimiters/"&gt;Electronics knowledge: Inrush current limiters&lt;/a&gt;, just exchanging the relay with a p-channel MOSFET, and adding a more defined time delay. The other one has a &amp;ldquo;linear&amp;rdquo; approach turning power on gradually using a PNP power transistor.&lt;/p&gt;&#10;&lt;h2 id="motivation-inrush-current-limiter-for-usb-pd"&gt;Motivation: Inrush current limiter for USB-PD&lt;/h2&gt;&#10;&lt;p&gt;As my experiments with &lt;a href="https://blog.schallbert.de/en/usbpd-explained/"&gt;USB-PD for AnywhereAmps&lt;/a&gt; failed, I have to create a circuit that limits inrush current to my amplifier before trying again.&lt;/p&gt;&#10;&lt;h2 id="circuits-explained"&gt;Circuits explained&lt;/h2&gt;&#10;&lt;p&gt;The two circuits are very similar. They only really differ in two components of the power section. Generally speaking, increasing delay capacitor &lt;code&gt;C4&lt;/code&gt; or increasing charge resistor &lt;code&gt;R9&lt;/code&gt; will prolong the delay time.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/inrush_circuit.jpg" alt="inrush current limiter sections"&gt;&lt;/figure&gt;&#10;&lt;h3 id="input-section-blue"&gt;Input section (blue)&lt;/h3&gt;&#10;&lt;p&gt;The input section features a transient &lt;a href="https://blog.schallbert.de/en/knowledge-tvsdiodes/"&gt;voltage suppressor diode&lt;/a&gt; to eliminate reverse current or overvoltage cases.&lt;/p&gt;&#10;&lt;h3 id="delay-section-yellow"&gt;Delay section (yellow)&lt;/h3&gt;&#10;&lt;p&gt;When switched on, &lt;code&gt;C4&lt;/code&gt; will gradually charge via &lt;code&gt;R9&lt;/code&gt;. When the NPN bipolar transistor &lt;code&gt;Q2&lt;/code&gt;&amp;rsquo;s threshold voltage is reached, it will slowly reduce voltage behind &lt;code&gt;R11&lt;/code&gt; which will in turn activate &lt;code&gt;M2&lt;/code&gt;, the power path of this circuit. &lt;code&gt;R12&lt;/code&gt; has been added to provide a stable operating point for &lt;code&gt;Q2&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="power-section-green"&gt;Power section (green)&lt;/h3&gt;&#10;&lt;p&gt;The linear variant has a PNP power transistor &lt;code&gt;Q1&lt;/code&gt; in the main current path (&lt;a href="https://blog.schallbert.de/en/inrushlimiter-circuits/#circuit-comparison"&gt;image below&lt;/a&gt;). It is activated via &lt;code&gt;Q2&lt;/code&gt; and the current it can provide is controlled using &lt;code&gt;R10&lt;/code&gt;, which in this case has a relatively low value to allow high emitter-collector currents.&lt;/p&gt;&#10;&lt;p&gt;While the switching variant uses a current limiting resistor &lt;code&gt;R8&lt;/code&gt; that is shorted by the p-channel mosfet &lt;code&gt;M2&lt;/code&gt;, it&amp;rsquo;s voltage-controlled nature does not require special care when selecting &lt;code&gt;R10&lt;/code&gt;. You should just make sure that it&amp;rsquo;s (negative) gate threshold voltage is well met.&lt;/p&gt;&#10;&lt;h3 id="simulated-load-purple"&gt;Simulated load (purple)&lt;/h3&gt;&#10;&lt;p&gt;My load is the power amplifier circuitry. To simulate its idle state and the powering-up behavior, I chose a 400Ohm resistor &lt;code&gt;R5&lt;/code&gt; and a 1200uF polarized capacitor &lt;code&gt;C3&lt;/code&gt;. The TVS diode protects from over voltage and reverse currents. I have tried the simulations with capacitor values of up to 10000uF and resistors down to 40Ohm, all with similar and good results.&lt;/p&gt;&#10;&lt;h2 id="circuit-comparison"&gt;Circuit comparison&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/inrushlimiter-variants.jpg" alt="inrush current limiter variants compared"&gt;&lt;/figure&gt;&#10;The blue line in the diagram is what is most relevant: It describes the output voltage of the system over time. Both circuits are tuned to reach target output voltage after 0.2 seconds. Still the curves differ quite a bit showing the different nature of the two approaches.&lt;/p&gt;&#10;&lt;h3 id="p-ch-mosfet-approach"&gt;P-CH MOSFET approach&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Due to switching operation, inrush current limiter is effective but not exactly smooth&lt;/li&gt;&#10;&lt;li&gt;power losses are very small for this variant because both triggering NPN transistor and power mosfet don&amp;rsquo;t use much power on their own.&lt;/li&gt;&#10;&lt;li&gt;wide range of usability for connected devices only limited by the mosfet&amp;rsquo;s maximum continuous current.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="pnp-power-transistor-approach"&gt;PNP power transistor approach&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Very smooth operation, turn-on time can be controlled very well because of the PNP&amp;rsquo;s large ohmic region&lt;/li&gt;&#10;&lt;li&gt;higher power losses when turned on due to high base current through &lt;code&gt;R10&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Not very versatile, as high load currents will create a considerable voltage drop over &lt;code&gt;Q1&lt;/code&gt;&amp;rsquo;s emitter-collector path.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="further-improvements"&gt;Further improvements&lt;/h3&gt;&#10;&lt;p&gt;To keep the versatility of this circuit high while at the same time keeping a smooth voltage curve, one could use an operational amplifier to monitor load current, regulating it down to allowed values if needed. The power mosfet could be kept while the disadvantage of the narrow ohmic operation band would vanish. The cost would be additional complexity like described &lt;a href="https://electronics.stackexchange.com/questions/408379/current-limiting-source-using-opamp" target="_blank" rel="noopener noreferrer" class="external-link"&gt;in this electronics stackexchange post&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;</description></item><item><title>USB Power Delivery</title><link>https://blog.schallbert.de/en/usbpd-explained/</link><pubDate>Thu, 27 Jan 2022</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/usbpd-explained/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/stusb4500-thumb.jpg"&#10; class="post-cover"&#10; alt="ST&amp;#39;&amp;#39;s EVAL-SCS001V1: STUSB4500 evaluation board"&#10; title="USB Power Delivery" /&gt;&#10;&lt;h2 id="usb-power-delivery"&gt;USB Power Delivery&lt;/h2&gt;&#10;&lt;p&gt;USB Power Delivery is a standard within the USB 3.x protocol that specifies power supply and voltage levels within the USB network.&#10;With USB-PD it is possible to daisy-chain consumers which then negotiate power demands with the source(s) automatically. The standard has different versions, of which the latest (2021) allows up to 5 Ampères at 3.3&amp;hellip;48V and a maximum of 240 Watts of power.&lt;/p&gt;&#10;&lt;h3 id="usb-pd-roles"&gt;USB-PD roles&lt;/h3&gt;&#10;&lt;p&gt;In the USB Power Delivery Standard, there are different roles that participants on the ends of an USB cable can play. They can be host or client for data purposes, and at the same time power source, power sink, or both for other participants.&lt;/p&gt;&#10;&lt;h3 id="role-terminology"&gt;Role terminology&lt;/h3&gt;&#10;&lt;p&gt;USB-C uses the acronym &lt;code&gt;DFP&lt;/code&gt; or Down-Facing Port to make clear that the port is the one of a &lt;a href="https://community.infineon.com/t5/Knowledge-Base-Articles/USB-Type-C-and-USB-PD-FAQs/ta-p/679219#." target="_blank" rel="noopener noreferrer" class="external-link"&gt;Host&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. &lt;code&gt;UFP&lt;/code&gt; or Up-Facing Port is used for clients that can be connected to hosts. There&amp;rsquo;a third role, though: &lt;code&gt;DRP&lt;/code&gt; or Dual-Role Port. This port can serve either as a host or as a client, depending on who the connecting partner on the other side of the cable is and what the USB network negotiates.&lt;/p&gt;&#10;&lt;p&gt;In USB-PD context, any of those ports can be configured to be sinking or sourcing current. Example: Although a Laptop&amp;rsquo;s USB-C port&amp;rsquo;s role may be configured to &lt;code&gt;DFP&lt;/code&gt;, when connected to a power source, it can still be a power sink. Hmm.&lt;/p&gt;&#10;&lt;p&gt;In this post, I&amp;rsquo;m only using the &lt;code&gt;UFP&lt;/code&gt; role in both USB-C and USB-PD word sense, meaning that I&amp;rsquo;m trying to configure a power sink for my device so that it will get the input voltage and current it needs.&lt;/p&gt;&#10;&lt;h3 id="usb-pd-version"&gt;USB-PD version&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Before USB-PD Standard 2.0, USB-C devices only support so-called &lt;em&gt;Power Profiles&lt;/em&gt; with fixed voltages of 5V, 12V, 20V and currents of up to 2A, 3A, or 5A.&lt;/li&gt;&#10;&lt;li&gt;Between USB-PD Standard 2.0 and USB-PD 3.0, &lt;em&gt;Power Delivery Objects&lt;/em&gt; replaced the old Power Profiles that allow different voltages/currents to be selected by the sink &lt;a href="https://usb.org/sites/default/files/D2T2-1%20-%20USB%20Power%20Delivery.pdf" target="_blank" rel="noopener noreferrer" class="external-link"&gt;depending on the power source&amp;rsquo;s offerings&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Voltages are fixed to 5V, 9V, 15V, 20V with current limits of 2.25A, 3A, 5A depending on source capability and voltage setting. PDOs allow fallback strategies, so if a source cannot support the highest power demand, the sink may select another PDO with less voltage/current that works for both source and sink.&lt;/li&gt;&#10;&lt;li&gt;With USB-PD Standard 3.0 PPS , the protocol offered even more flexibility with &lt;em&gt;Programmable Power Supply&lt;/em&gt;. Here, a power sink requests a voltage level between 3.3V and 21V, selectable in 20mV steps and 0.2 to 5A, selectable in 50mA steps while staying fully backwards-compatible to USB-PD 2.0&amp;rsquo;s Power Delivery Objects.&lt;/li&gt;&#10;&lt;li&gt;Latest USB-PD Standard 3.1 even enhances Programmable Power Supplies&amp;rsquo; limits and allows 3.3V - 48V and up to 5A &lt;a href="https://www.thephonetalks.com/usb-pd-2-0-vs-3-0-vs-3-1/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;using AVS&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; - &lt;em&gt;Adaptable Voltage Files&lt;/em&gt;. Whow.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Not all sources (wall plugs, powerbanks etc.) can supply all Voltage levels or currents, or even share the same standard of implementation. But a number of values is pretty common: 5V, 9V, 12V, 15V, 20V. Some of the products I have seen on the market offer up to 4A, but most keep it with up to 3A and voltages up to 15V are most common (in 2021).&lt;/p&gt;&#10;&lt;h2 id="power-delivery-asics"&gt;Power delivery ASICs&lt;/h2&gt;&#10;&lt;p&gt;ASIC stands for application specific integrated circuit, and in this post I&amp;rsquo;m trying to shed some light on ASICs with Power Delivery sink role that can work stand-alone, that is without an external control device such as a microprocessor. I found three types of these devices:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Standalone USB-PD sink, voltage and current setting via resistor cascade&lt;/li&gt;&#10;&lt;li&gt;Standalone USB-PD sink, voltage and current map programmable via data bus (&lt;a href="https://en.wikipedia.org/wiki/I%C2%B2C" target="_blank" rel="noopener noreferrer" class="external-link"&gt;I2C&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;/&lt;a href="https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter" target="_blank" rel="noopener noreferrer" class="external-link"&gt;UART&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;/&lt;a href="https://en.wikipedia.org/wiki/Serial_Peripheral_Interface" target="_blank" rel="noopener noreferrer" class="external-link"&gt;SPI&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;) into non-volatile memory of the ASIC&lt;/li&gt;&#10;&lt;li&gt;Standalone USB-PD sink with integrated microprocessor that needs to be programmed in order to work properly&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;What all ASICs have in common: They require peripheral circuitry to work - at least some resistors, capacitors, and power transistors to switch power supply and negotiate with connected power sources.&lt;/p&gt;&#10;&lt;h3 id="usb-pd-is-complex-versioning-and--power-contract-negotiations"&gt;USB-PD is complex: Versioning and power contract negotiations&lt;/h3&gt;&#10;&lt;p&gt;Both the downwards compatibility and the scaleability of USB-PD take their tribute - the USB-PD specification is a &amp;gt;600 page monster and it&amp;rsquo;s not always clear which source or sink supports which protocol level.&lt;/p&gt;&#10;&lt;p&gt;As both the source, sink, and the cable have to negotiate a Power Delivery contract based on the sink&amp;rsquo;s requirements, the source&amp;rsquo;s power options, and the cable&amp;rsquo;s capabilities, you can imagine that this process is very complex. Even more so as all participants may have implemented a different USB-PD standard, revision and version. Fun fact: The USB Power Delivery specification package&amp;rsquo;s zip folder on the &lt;a href="https://usb.org/document-library/usb-power-delivery" target="_blank" rel="noopener noreferrer" class="external-link"&gt;official page&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; has a file size of &amp;gt;42MB. Although I think this topic is very interesting, I don&amp;rsquo;t have the time to deep-dive into the specifics 😅&lt;/p&gt;&#10;&lt;p&gt;So, let&amp;rsquo;s leave all this stuff behind and try finding an easy-to-implement solution.&lt;/p&gt;&#10;&lt;h3 id="example-asics"&gt;Example ASICs&lt;/h3&gt;&#10;&lt;p&gt;I have arbitrarily chosen three ICs from the web, supplied by different companies, one each per device type:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Cypress/Infinion&amp;rsquo;s &lt;a href="https://www.infineon.com/cms/en/product/universal-serial-bus-usb-power-delivery-controller/usb-c-and-power-delivery/ez-pd-barrel-connector-replacement-bcr/cypd3177-24lqxqt/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;CYPD3177&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, a resistor-config USB-PD sink controller&lt;/li&gt;&#10;&lt;li&gt;STMicroelectronics&amp;rsquo; &lt;a href="https://www.st.com/en/interfaces-and-transceivers/stusb4500.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;STUSB4500&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, a Memory-config USB-PD sink controller&lt;/li&gt;&#10;&lt;li&gt;Microchip&amp;rsquo;s &lt;a href="https://www.microchip.com/en-us/product/UPD301C#document-table" target="_blank" rel="noopener noreferrer" class="external-link"&gt;UPD301C&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, a fully programmable USB-PD dual role controller&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I&amp;rsquo;ll select the STUSB4500 first.&lt;/p&gt;&#10;&lt;h3 id="chip--ic-shortage"&gt;Chip / IC Shortage&lt;/h3&gt;&#10;&lt;p&gt;For integration into my design, I wanted to buy some samples. Unfortunately, Chip shortages hit hard so that neither of the distributors that I normally order electronics parts with, had any in stock for the next year.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/digikey_stusb4500.jpg" alt="STUSB4500 out of stock at DigiKey"&gt;&lt;/figure&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/st_stusb4500.jpg" alt="STUSB4500 out of stock at ST"&gt;&lt;/figure&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/farnell_stusb4500.jpg" alt="STUSB4500 out of stock at Farnell"&gt;&lt;/figure&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/mouser_stusb4500.jpg" alt="STUSB4500 out of stock at Mouser"&gt;&lt;/figure&gt;&#10;That&amp;rsquo;s why I decided to order a development board with the ASIC readily mounted. This is 10x more expensive than the chip itself but I didn&amp;rsquo;t have a choice here.&lt;/p&gt;&#10;&lt;h3 id="stusb4500"&gt;Integration of STUSB4500&lt;/h3&gt;&#10;&lt;p&gt;STUSB4500 is using the USB-PD Standard 2.0 - so not the latest greatest one - but downwards compatibility of later standards make it a quick and easy starting point for a maker.&#10;The device comes pre-programmed with three Power Delivery Objects:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;PDO1 = 5V/1.5A&lt;/li&gt;&#10;&lt;li&gt;PDO2 = 15V/1.5A&lt;/li&gt;&#10;&lt;li&gt;PDO3 = 20V/1A&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;So for my &lt;a href="https://blog.schallbert.de/en/projects/aa_alpha_1_1/"&gt;AnywhereAmps Alpha 1.1&lt;/a&gt; evolution 1.2 I can use this thing right out of the box without re-programming. If I wanted to change the Power Delivery Objects, I&amp;rsquo;d use an Arduino board with I2C and install STUSB4500 library which is readily available e.g. at &lt;a href="https://registry.platformio.org/libraries/ardnew/STUSB4500" 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;. With this, I could even activate USB-PD 3.0&amp;rsquo;s Programmable Power Supply if I wanted. Let&amp;rsquo;s see, when I find the time, I&amp;rsquo;ll prepare a specific blog entry for programming STUSB4500.&lt;/p&gt;&#10;&lt;h3 id="powering-up-the-amplifier"&gt;Powering up the amplifier&lt;/h3&gt;&#10;&lt;p&gt;OK, so I connected the eval board to a 15V capable 30V USB-C power supply. The purple light indicates 15V available on the output rail.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/stusb4500_on.jpg" alt="Usb-PD Evaluation Board STUSB4500 stock, 15V mode"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;p&gt;Now I attached my amplifier circuits at its output terminals for an initial test. STUSB evaluation board lights gone, amp off. Meh. At least I have an indicaton for what the cause might be: It&amp;rsquo;s &lt;a href="https://blog.schallbert.de/en/capacitor-problems/"&gt;capacitive inrush currents, again&lt;/a&gt;! To be able to properly power devices with large bulk capacitors, either I&amp;rsquo;ll have to set current in the PDO to abormally high levels or I make up my mind aboud constructing a proper, miniaturized inrush limiter circuit. I have setup a prototype already that I discuss &lt;a href="https://blog.schallbert.de/en/knowledge-inrushlimiters/"&gt;here&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="should-you-use-usb-pd-for-your-projects"&gt;Should you use USB-PD for your projects?&lt;/h2&gt;&#10;&lt;p&gt;The answer is a definete &amp;ldquo;it depends&amp;rdquo;. Although the capabilities of USB-PD are great, there are a few drawbacks:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;requires an extra ASIC with periphery to negotiate the power contracts. This takes PCB space and increases project costs significantly&lt;/li&gt;&#10;&lt;li&gt;To be fully backwards compatible, it makes sense to buy an ASIC that both supports Standard 3.x, and ideally, you take one that is programmable&lt;/li&gt;&#10;&lt;li&gt;If you take a programmable device, there&amp;rsquo;s an additional learning curve for software development, the ASIC-specific libraries you want to use, and possibly even the need for programmer hardware&lt;/li&gt;&#10;&lt;li&gt;You have to make sure that power supply and cable are compatible to your USB-PD sink&amp;rsquo;s needs. Cables for high currents are expensive, too&lt;/li&gt;&#10;&lt;li&gt;USB-C port is miniaturized. SMD soldering and entry-level soldering stations might not become friends ever, so you should bring some experience on that topic.&lt;/li&gt;&#10;&lt;li&gt;Barrel-jacks are just round and thus much easier to mount in an enclosure than a PCB with attached USB-C socket that also has to withstand considerable physical strains.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;On the other hand, using USB-PD makes much sense when you&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;have a mobile gadget that you want to take along with you, or that is often connected to and disconnected from power sources&lt;/li&gt;&#10;&lt;li&gt;want the flexibility of battery-powered devices: Just connect a compatible power bank and off you go&lt;/li&gt;&#10;&lt;li&gt;need a data connection to your device anyways&lt;/li&gt;&#10;&lt;li&gt;have special constraints on your power supply and want very specific voltage/currents to be delivered&lt;/li&gt;&#10;&lt;li&gt;want easy overcurrent and overvoltage protection out of the box&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item><item><title>Metabo / Cordless Alliance Systems</title><link>https://blog.schallbert.de/en/no-can-use-metabo/</link><pubDate>Thu, 09 Dec 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/no-can-use-metabo/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/metabo-thumb.jpg"&#10; class="post-cover"&#10; alt="Metabo logo, image courtesy of Metabo"&#10; title="Metabo / Cordless Alliance Systems" /&gt;&#10;&lt;h2 id="power-source-for-anywhereamps"&gt;Power Source for AnywhereAmps&lt;/h2&gt;&#10;&lt;h3 id="status-quo"&gt;Status quo&lt;/h3&gt;&#10;&lt;p&gt;I have used Metabo / CAS (&lt;a href="https://cordless-alliance-system.de/en" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Cordless Alliance System&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;) batteries for both AnywhereAmps &lt;a href="https://blog.schallbert.de/en/projects/aa_alpha/"&gt;Alpha 1.0&lt;/a&gt; and &lt;a href="https://blog.schallbert.de/en/projects/aa_alpha_1_1/"&gt;Alpha 1.1&lt;/a&gt;. The latter already uses the power supervisor IC &lt;a href="https://blog.schallbert.de/en/configure-max1756x/"&gt;MAX17562&lt;/a&gt;, protecting both battery and circuits from reverse current, overcurrent, and undervoltage.&lt;/p&gt;&#10;&lt;h3 id="problem"&gt;Problem&lt;/h3&gt;&#10;&lt;p&gt;Although using the power supervisor IC was a big leap forward, still it would be better if I was able to read battery &lt;a href="https://en.wikipedia.org/wiki/State_of_charge" target="_blank" rel="noopener noreferrer" class="external-link"&gt;state of charge&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; directly. This wouldn&amp;rsquo;t just enable safe operation, but also allow auto-switching the amplifier off when the battery goes low without side effects like auto-restart when battery voltage jumps back up when load current drops on shutdown.&lt;/p&gt;&#10;&lt;h2 id="discussion-with-metabocas"&gt;Discussion with Metabo/CAS&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/metabo.jpg" alt="Metabo logo"&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Metabo logo&lt;/span&gt;&lt;a&#10; href="https://www.metabo.com/de/de/info/aktuell/media/logos/"&#10; class="attr-link"&#10; aria-label="Attribution 1"&#10; &gt;&#10; &lt;sup class="attr-id"&gt;[1]&lt;/sup&gt;&#10; &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;That&amp;rsquo;s why I contacted the Metabo CAS team, asking whether I could get some support and the specifications for communicating with the battery pack.&#10;I was quite happy to see that they respond really quickly, and soon I was on a phone call discussing the matter.&lt;/p&gt;&#10;&lt;h3 id="outcome"&gt;Outcome:&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Usage of CAS batteries in other devices than the alliance&amp;rsquo;s power tool families is considered a &amp;ldquo;hack&amp;rdquo; and thus wouldn&amp;rsquo;t be approved by Metabo&lt;/li&gt;&#10;&lt;li&gt;Metabo is interested in cordless tools manufacturers to join their battery alliance, but my application in an amplifier is just too far away (Metabo&amp;rsquo;s slogan once was &lt;em&gt;&amp;ldquo;Work, don&amp;rsquo;t play&amp;rdquo;&lt;/em&gt;)&lt;/li&gt;&#10;&lt;li&gt;Metabo would expect me to produce 10k+ units per year before I could joind the alliance - which is just unrealistic&lt;/li&gt;&#10;&lt;li&gt;I&amp;rsquo;d have to understand, but sharing the specifications and allowing me to use the interfaces would draw some of their resources while they wouldn&amp;rsquo;t have any expectations on return of invest&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;As disappointing 😔 as this is, it didn&amp;rsquo;t come totally unexpected. The engineer on the phone made time for me. He told me that they&amp;rsquo;d appreciate my enthusiasm about their battery systems very much and that I shouldn&amp;rsquo;t feel discouraged.&lt;/p&gt;&#10;&lt;p&gt;Well. Let&amp;rsquo;s talk about an&lt;/p&gt;&#10;&lt;h2 id="alternative"&gt;Alternative&lt;/h2&gt;&#10;&lt;p&gt;I know only one other power standard also featuring battery powered systems that meets my expectations below.&lt;/p&gt;&#10;&lt;h3 id="requirements"&gt;Requirements&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&amp;ldquo;open enough&amp;rdquo; for my usage&lt;/li&gt;&#10;&lt;li&gt;safe&lt;/li&gt;&#10;&lt;li&gt;of high quality&lt;/li&gt;&#10;&lt;li&gt;meets my purpose&amp;rsquo;s voltage and current demands&lt;/li&gt;&#10;&lt;li&gt;widely available&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://upload.wikimedia.org/wikipedia/commons/thumb/1/10/USB_Type-C_Charging_Logo.svg/330px-USB_Type-C_Charging_Logo.svg.png" alt="USB-PD logo"&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;USB-PD logo&lt;/span&gt;&lt;a&#10; href="https://commons.wikimedia.org/wiki/File:USB_Type-C_Charging_Logo.svg#file"&#10; class="attr-link"&#10; aria-label="Attribution 2"&#10; &gt;&#10; &lt;sup class="attr-id"&gt;[2]&lt;/sup&gt;&#10; &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h3 id="usb-power-delivery"&gt;USB Power Delivery&lt;/h3&gt;&#10;&lt;p&gt;The standard is called &lt;a href="https://en.wikipedia.org/wiki/USB_hardware#USB_power_delivery" target="_blank" rel="noopener noreferrer" class="external-link"&gt;USB Power Delivery&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Although it went through my mind earlier, I never wanted to actually use this because it involves massive additional work.&lt;/p&gt;&#10;&lt;p&gt;USB Power Delivery uses a complex protocol to establish a power contract between source and sink devices. All this has to be developed, a specialized IC has to be selected and peripheral circuits need to be designed for this to make it work with my setup.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-12-09_st_stusb4500.jpg" alt="STUSB4500 image example for USB-PD sink IC"&gt;&lt;/figure&gt;&#10;&lt;p&gt;😱 Oh no, they are out of stock!&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;ll write a separate blog post once I got a grip on this new subject.&lt;/p&gt;&#10;</description></item><item><title>Configure MAX1756x for your application</title><link>https://blog.schallbert.de/en/configure-max1756x/</link><pubDate>Sat, 04 Dec 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/configure-max1756x/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-12-04_max1756x-thumb.jpg"&#10; class="post-cover"&#10; alt="Max1756x typicall application, image courtesy of Maxim Integrated"&#10; title="Configure MAX1756x for your application" /&gt;&#10;&lt;h2 id="power-supervisor-configuration-made-simple"&gt;Power Supervisor configuration made simple&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-12-04_max17562.jpg" alt="MAX17562 on my printed circuit board for AnywhereAmps"&gt;&lt;/figure&gt;&#10;I use MAX17562 ICs in my design for &lt;a href="https://blog.schallbert.de/en/anywhereamps-get-pcb/"&gt;AnywhereAmps Alpha 1.1&lt;/a&gt;. For you, I made a quick summary of MAX1756x features and how to configure them. 🙂&lt;/p&gt;&#10;&lt;p&gt;Of course, the datasheet is the master of all information I provide below. I&amp;rsquo;ll just add some design considerations, details on my way of using it, and provide application proposals.&lt;/p&gt;&#10;&lt;h3 id="what-are-they-again"&gt;What are they, again?&lt;/h3&gt;&#10;&lt;p&gt;&lt;a href="https://www.analog.com/en/products/MAX17562.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;MAX1756x&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; is an adjustable overvoltage/ undervoltage and overcurrent protector IC. It is available in three different versions for handling over current events:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;MAX17561: Will turn off in an overcurrent event and autoretry after typically &lt;code&gt;600ms&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;MAX17562: Will latch off in an overcurrent event; restart after enable cycle&lt;/li&gt;&#10;&lt;li&gt;MAX17563: Will continue normal operation with current limiter active (dissipates heat)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Both versions are limiting overcurrent events to the configured current limit.&lt;/p&gt;&#10;&lt;h3 id="use-my-max1756x-application-tool"&gt;Use my MAX1756x application tool!&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-12-04_spreadsheet.jpg" alt="Max1756x application calculator spreadsheet"&gt;&lt;/figure&gt;&#10;To simplify usage of Max1756x IC in your application, you may use my &lt;a href="https://github.com/Schallbert/AnywhereAmps/blob/master/Preamp/max1756x_application_calculator.xlsx" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Max1756x application calculator spreadsheet&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&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;h3 id="max1756x-current-limiter--overload-detector"&gt;MAX1756x current limiter / overload detector&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-12-04_seti.jpg" alt="SETI resistor configuration from MAX1756x datasheet"&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Courtesy of Maxim Integrated: SETI resistor config&lt;/span&gt;&lt;a&#10; href="https://www.analog.com/media/en/technical-documentation/data-sheets/MAX17561-MAX17563.pdf"&#10; class="attr-link"&#10; aria-label="Attribution 1"&#10; &gt;&#10; &lt;sup class="attr-id"&gt;[1]&lt;/sup&gt;&#10; &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;The IC features an adjustable current limiter and overcurrent detection (&lt;code&gt;SETI&lt;/code&gt;). When current increases above &lt;code&gt;SETI&lt;/code&gt; threshold, it is limited to the set value. For MAX17561 and MAX17562: In case this condition remains true for more than typically &lt;code&gt;20.7ms&lt;/code&gt; blanking time, the device will turn off the load circuit. As the MAX17563 will continue normal operation, its power dissipation might lead to over temperature shutdown at some point.&lt;/p&gt;&#10;&lt;p&gt;This is how you configure the current limiter / overload detection using the &lt;code&gt;SETI&lt;/code&gt; input:&#10;Select a resistor based on the curve on the right (taken from the datasheet) and connect it to &lt;code&gt;SETI&lt;/code&gt; and &lt;code&gt;GND&lt;/code&gt;. For AnywhereAmps Alpha 1.2, I&amp;rsquo;m using a &lt;code&gt;3k3&lt;/code&gt; resistor to get a current limit of &lt;code&gt;~3.5A&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="max1756x-undervoltage-configuration"&gt;MAX1756x undervoltage configuration&lt;/h3&gt;&#10;&lt;p&gt;The IC features undervoltage lockout (&lt;code&gt;UVLO&lt;/code&gt;). When voltage decreases below &lt;code&gt;UVLO&lt;/code&gt; threshold for typically &lt;code&gt;16.7ms&lt;/code&gt;, the device&amp;rsquo;s power transistors will switch off the load circuit. Internally, the IC will compare the input voltage at the &lt;code&gt;UVLO&lt;/code&gt; pin with the reference voltage &lt;code&gt;Vbg = 1.2V&lt;/code&gt;. To set the undervoltage lockout threshold, use a voltage divider to keep &lt;code&gt;UVLO&lt;/code&gt; level above &lt;code&gt;Vbg&lt;/code&gt; during normal operation.&lt;/p&gt;&#10;&lt;h3 id="max1756x-overvoltage-configuration"&gt;MAX1756x overvoltage configuration&lt;/h3&gt;&#10;&lt;p&gt;The IC features overvoltage lockout (&lt;code&gt;OVLO&lt;/code&gt;). When voltage increases above &lt;code&gt;OVLO&lt;/code&gt; threshold for typically &lt;code&gt;16.7ms&lt;/code&gt;, the device&amp;rsquo;s power transistors will switch off the load circuit.&#10;It works identical to the undervoltage lockout with the only difference that voltage at the &lt;code&gt;OVLO&lt;/code&gt; pin should remain below &lt;code&gt;Vbg&lt;/code&gt; under normal operating conditions.&lt;/p&gt;&#10;&lt;h3 id="max1756x-reverse-current-protection"&gt;MAX1756x reverse current protection&lt;/h3&gt;&#10;&lt;p&gt;The IC&amp;rsquo;s reverse current protection can be enabled by pulling the &lt;code&gt;*RIEN&lt;/code&gt; pin to GND. If you intend to use this feature, remember to not accidentally use polarized bypass capacitors &lt;a href="https://blog.schallbert.de/en/capacitor-problems/#explosion"&gt;like I did&lt;/a&gt; at the IC&amp;rsquo;s input as they will explode if you apply reverse voltage.&lt;/p&gt;&#10;&lt;h3 id="fault-flag-capability"&gt;Fault *FLAG capability&lt;/h3&gt;&#10;&lt;p&gt;MAX1756x will indicate a power fault using its &lt;code&gt;*FLAG&lt;/code&gt; port under the following conditions:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Thermal overload&lt;/li&gt;&#10;&lt;li&gt;Overvoltage lockout defined through &lt;code&gt;OVLO&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Reverse current: when reverse current protection &lt;code&gt;*RIEN&lt;/code&gt; has been enabled&lt;/li&gt;&#10;&lt;li&gt;Overload current defined through&lt;code&gt;SETI&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="recommendation-use-tvs-diodes"&gt;Recommendation: Use TVS diodes&lt;/h3&gt;&#10;&lt;p&gt;As the datasheet recommends and &lt;a href="https://blog.schallbert.de/en/capacitor-problems/#inductance"&gt;my experience from incidents in the past&lt;/a&gt; shows, I&amp;rsquo;d use &lt;code&gt;27V&lt;/code&gt; &lt;a href="https://blog.schallbert.de/en/knowledge-tvsdiodes/"&gt;Transient Voltage Suppressor diodes&lt;/a&gt; in parallel to both MAX1756x&amp;rsquo;s input and output ports. It will protect (any inductive) load circuitry from negative voltage spikes on shutdown and the IC itself from surge voltages when connecting a live power supply. Plus, it will clamp reverse input voltage to non-harmful levels.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-12-04_tvs.jpg" alt="TVS usage in MAX1756x circuit"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;h3 id="recommendation-2-use-non-polarized-input-capacitors"&gt;Recommendation 2: Use non-polarized input capacitors&lt;/h3&gt;&#10;&lt;p&gt;To mitigate running reset issues when connected to &amp;ldquo;weak&amp;rdquo; power supplies or high-resistance input power wiring, use bigger non-polarized input bypass capacitors than you normally would (no, &lt;code&gt;100nF&lt;/code&gt; might not be enough!) - Especially if you drive capacitive or other loads that cause input surge currents.&lt;/p&gt;&#10;&lt;p&gt;For more information how to avoid problems here, please read my knowledge blog entry about &lt;a href="https://blog.schallbert.de/en/knowledge-inrushlimiters/"&gt;inrush current limiters&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;Disclaimer: Although I thorougly read the datasheet and carefully added the data, I can of course take no guarantee whatsoever for the results you yield from this tool.&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>Inrush Current Limiter - Part1</title><link>https://blog.schallbert.de/en/knowledge-inrushlimiters/</link><pubDate>Sun, 14 Nov 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/knowledge-inrushlimiters/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-11-14_bulb-thumb.jpg"&#10; class="post-cover"&#10; alt="Electrical light bulb, 12V 5W"&#10; title="Inrush Current Limiter - Part1" /&gt;&#10;&lt;h2 id="electronics-knowledge-article-inrush-current-limiters"&gt;Electronics knowledge article: Inrush current limiters&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2021-11-14_inrush_current_limiter_relay.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Action! Using a relay based inrush current limiter.&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h3 id="what-is-it"&gt;What is it?&lt;/h3&gt;&#10;&lt;p&gt;A so-called &lt;a href="https://en.wikipedia.org/wiki/Inrush_current" target="_blank" rel="noopener noreferrer" class="external-link"&gt;inrush current&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; is the surge current that flows through a circuit directly after start-up until the circuit has reached its steady state. High inrush currents are often unwanted because they can overload circuit parts reducing their lifetime, create electromagnetic interferences, and cause a short-term voltage drop that may lead to running resets of a controlled device &lt;a href="https://blog.schallbert.de/en/capacitor-problems/"&gt;like described here&lt;/a&gt; or to other undesired behaviour.&lt;/p&gt;&#10;&lt;h3 id="example"&gt;Example&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://upload.wikimedia.org/wikipedia/commons/f/f9/Lamp_inrush_current.png" alt="Wikipedia: inrush current of an incandescent light bulb"&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Oscillograph: inrush current of an incandescent light bulb&lt;/span&gt;&lt;a&#10; href="https://commons.wikimedia.org/wiki/File:Lamp_inrush_current.png#file"&#10; class="attr-link"&#10; aria-label="Attribution 1"&#10; &gt;&#10; &lt;sup class="attr-id"&gt;[1]&lt;/sup&gt;&#10; &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;Incandescent light bulb: The pink line is the power supply&amp;rsquo;s output voltage, the yellow line traces current. You can see that the power supply decreases output voltage to reduce current draw during startup. After &lt;code&gt;~100ms&lt;/code&gt;, the voltage stabilizes as current flow through the lamp still reduces a bit while the filament slowly reaches steady state temperature.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-11-14_bulb_resistance.jpg" alt="Measuring the resistance of an incandescent light bulb"&gt;&lt;/figure&gt;&#10;&lt;h3 id="background"&gt;Background&lt;/h3&gt;&#10;&lt;p&gt;The bulb&amp;rsquo;s filament acts like a positive temerature coefficient (PTC) resistor: It has a very low resistance at room temperature and a much higher resistance at its normal operating temperature of &lt;a href="https://hypertextbook.com/facts/1999/AlexanderEng.shtml" target="_blank" rel="noopener noreferrer" class="external-link"&gt;~3300K&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&#10;A small experiment proves this:&#10;If I take a &lt;code&gt;116W&lt;/code&gt; halogen incandescent light bulb, its resistance in &amp;ldquo;steady state&amp;rdquo; at &lt;code&gt;230V&lt;/code&gt; is around \(R_{steady} = \frac{U^2}{P} = 456{Ohm}\).&#10;When I take its resistance using a multimeter in the switched-off state, I measure \(R_{off} = 38.6{Ohm}\).&#10;While this lamp normally consumes \(I_{steady} = 0.5A\) of current, at the moment I switch it on, it will draw about \(I_{inrush} = 6A\) which is 12 times as much!&lt;/p&gt;&#10;&lt;h2 id="inrush-current-limiter-circuits"&gt;Inrush current limiter circuits&lt;/h2&gt;&#10;&lt;p&gt;Inrush current limiter circuits are quite often used in devices with big inductive or capacitive loads.&#10;&lt;a href="https://www.ti.com/lit/an/slvae30e/slvae30e.pdf" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Examples&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; are transformers, motors (inductive), power amplifiers, switching power suply (capacitive) etc.&lt;/p&gt;&#10;&lt;p&gt;There are &lt;a href="https://e2e.ti.com/blogs_/b/powerhouse/posts/powertips-how-to-limit-inrush-current-in-an-ac-dc-power-supply" target="_blank" rel="noopener noreferrer" class="external-link"&gt;several types of inrush current limiters (TI application note)&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; with varying complexity.&lt;/p&gt;&#10;&lt;h3 id="negative-temperature-coefficient-resistor-ntc--thermistor"&gt;Negative temperature coefficient resistor (NTC) / thermistor&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--left"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/ntc.jpg" alt="Electric symbol of a Negative temperature-coefficient thermistor"&gt;&lt;/figure&gt;&#10;A negative temperature coefficient thermistor inrush current limiter is the simplest circuit in this comparison. It consists of only one additional part. The NTC&amp;rsquo;s resistance vs. temperature behavior is opposite to the one of the above incandescent light bulb example: its resistance reduces with increasing temperature, allowing more current to flow through a circuit.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-11-14_NTC.jpg" alt="NTC curve example"&gt;&lt;/figure&gt;&#10;It is placed in series connection with the load. Most NTC&amp;rsquo;s resistance vs temperature behavior is logarithmic, so its resistance is by far lower at high temperatures. This way, it very effectively reduces inrush currents at the cost of additional power dissipation as the NTC will continue self-heating, keeping its low resistance.&#10;This kind of circuit is used where the additional power dissipation is not an issue, and where system complexity shall be kept low while cost efficiency is at its best.&lt;/p&gt;&#10;&lt;h3 id="limiter-using-a-start-up-relay"&gt;Limiter using a Start-up relay&lt;/h3&gt;&#10;&lt;p&gt;This circuit avoids the disadvantages of additional series resistance to a load by adding a power resistor and a relay. It is typically used in high-power DC supplies or sensitive devices like amplifiers.&#10;The relay contact is normally open and wired in parallel to the power resistor which has a value of typically &lt;code&gt;1...22Ohm&lt;/code&gt; depending on the load. Behind this, the load is connected which causes the inrush current to occur. For this example, I take a car&amp;rsquo;s light bulb as load. When switching the device on, the resistor will limit circuit current and voltage on the load side will gradually increase. Once the voltage at the NPN &lt;code&gt;BC337&lt;/code&gt; transistor&amp;rsquo;s base is high enough, it will switch on, allowing current to flow through the relay coil which then short-circuits the power resistor so that the load can unleash its full power potential.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-11-14_relay.jpg" alt="Circuit plan for a start-up relay inrush current limiter"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Calculation of &lt;code&gt;R2, R3&lt;/code&gt; depends on the steady state voltage you get behind &lt;code&gt;R1&lt;/code&gt; when relay coil is off, the characteristics of the transistor you choose, and the current the relay needs to work properly. In the example I chose&lt;code&gt;R2 = 1kOhm&lt;/code&gt; as I need the transistor to safely drive &lt;code&gt;~40mA&lt;/code&gt; to the relay, so I couldn&amp;rsquo;t select any higher value having &lt;code&gt;BC337&lt;/code&gt;&amp;rsquo;s current amplification value in mind. But I didn&amp;rsquo;t want to have it switched on early, so I added &lt;code&gt;R3&lt;/code&gt;. The higher its value, the earlier the relay will activate, but choosing its value too low will reduce &lt;code&gt;BC337&lt;/code&gt;&amp;rsquo;s base voltage so much that it cannot even switch.&lt;/p&gt;&#10;&lt;p&gt;A similar circuit is used in this &lt;a href="https://www.elektronik-kompendium.de/public/schaerer/onilim.htm" target="_blank" rel="noopener noreferrer" class="external-link"&gt;application note for a toroidal transformer soft start&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Here&amp;rsquo;s how the circuit looks like in real life:&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-11-14_relay_circuit.jpg" alt="Start-up relay circuit on a breadboard"&gt;&lt;/figure&gt;&#10;I&amp;rsquo;ve taken the liberty to add a switch so controlling circuit power supply would become more comfortable to me. The relay features a freewheeling diode to protect the transistor from negative voltage spikes.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-11-14_compare_inrush.jpg" alt="Comparison of inrush currents with and without limiter"&gt;&lt;/figure&gt;&#10;The inrush current issue is actually solved. The comparison (yellow line) shows an unregulated inrush current of &lt;code&gt;3.5A&lt;/code&gt;, while the limiter reduces it to &lt;code&gt;0.8A&lt;/code&gt; which is just slightly above nominal current. A power resistor with smaller value would have reduced this even more.&lt;/p&gt;&#10;&lt;p&gt;The green line indicates &lt;code&gt;BC337&lt;/code&gt;&amp;rsquo;s base voltage. You can see that it slowly increases (Oscilloscope accuracy at &lt;code&gt;40VPP&lt;/code&gt; is not the best), and will turn on the relay when current flow (yellow line) has stabilized.&lt;/p&gt;&#10;&lt;h3 id="soft-start-circuits--configurable-current-limiters"&gt;Soft-start circuits / configurable current limiters&lt;/h3&gt;&#10;&lt;p&gt;There are dedicated ICs that help soft-starting a system, depending on&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Wattage needs&lt;/li&gt;&#10;&lt;li&gt;Ramp up time&lt;/li&gt;&#10;&lt;li&gt;Slew rate constraints&lt;/li&gt;&#10;&lt;li&gt;Total current limit&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;In addition, there are different types of inrush current limiters to be selected based on your application:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Time-controlled: System is fully turned-on after a specified time (simple but dumb)&lt;/li&gt;&#10;&lt;li&gt;Voltage-controlled: System is fully switched on above a voltage trigger level (still simple, medium cost)&lt;/li&gt;&#10;&lt;li&gt;Current-controlled: Limits system current or switches system fully on based on current trigger level (likely more expensive)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Thus, a fitting IC is to be selected. They may be called &lt;code&gt;Power Supervisors&lt;/code&gt;, &lt;code&gt;IC soft starter&lt;/code&gt;, &lt;code&gt;Supervisor &amp;amp; Reset IC&lt;/code&gt; or similar. They may even have internal switching power MOSFETs and use minimal peripheral circuitry. I&amp;rsquo;ve been working with &lt;a href="https://www.analog.com/en/products/MAX17562.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;MAX17562&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; for my &lt;a href="https://blog.schallbert.de/en/anywhereamps-get-pcb/#power-supervisor"&gt;evolution of AnywhereAmps&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Continue reading here: &lt;a href="https://blog.schallbert.de/en/inrushlimiter-circuits/"&gt;Inrush current limiters - Part2&lt;/a&gt;&lt;/p&gt;&#10;</description></item><item><title>Transient Voltage Suppressors</title><link>https://blog.schallbert.de/en/knowledge-tvsdiodes/</link><pubDate>Sat, 13 Nov 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/knowledge-tvsdiodes/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-11-02_tvs-thumb.jpg"&#10; class="post-cover"&#10; alt="Typical TVS voltage diagram"&#10; title="Transient Voltage Suppressors" /&gt;&#10;&lt;h2 id="electronics-knowledge-article-tvs-diode"&gt;Electronics knowledge article: TVS diode&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/tvs_diode.jpg" alt="Electric symbol for TVS diodes"&gt;&lt;/figure&gt;&#10;&lt;h3 id="what-is-it"&gt;What is it?&lt;/h3&gt;&#10;&lt;p&gt;A transient voltage suppressor diode protects a circuit from short-term voltage spikes. TVS diodes are available for different voltage settings, peak currents, and with uni- or bidirectional protection capability.&lt;/p&gt;&#10;&lt;h3 id="differences-between-tvs-and-zener-diodes"&gt;Differences between TVS and Zener diodes&lt;/h3&gt;&#10;&lt;p&gt;Like &lt;a href="https://en.wikipedia.org/wiki/Zener_diode" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Zener diodes&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, TVS diodes are normally operating in reverse bias direction. They also share the &lt;a href="https://electronics.stackexchange.com/questions/233600/differences-between-tvs-diode-and-zener-diodes-in-diagrams-and-in-practice" target="_blank" rel="noopener noreferrer" class="external-link"&gt;same electronics symbol&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;But while Zener diodes are designed to work at their precisely defined breakdown voltage with a constant control current in applications where voltage regulation is needed, TVS diodes normally operate below breakdown voltage as they are protective devices, &lt;a href="https://toshiba.semicon-storage.com/content/dam/toshiba-ss-v2/master/en/semiconductor/knowledge/e-learning/discrete/discrete-basic-chap2_en.pdf" target="_blank" rel="noopener noreferrer" class="external-link"&gt;clamping overvoltage spikes to acceptable levels&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. They are designed to withstand high peak pulse currents.&lt;/p&gt;&#10;&lt;h3 id="differences-between-tvs-and-schottky-diodes"&gt;Differences between TVS and Schottky diodes&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/schottky_diode.jpg" alt="Schottky diode electronics symbol"&gt;&lt;/figure&gt;&#10;The symbol for &lt;a href="https://en.wikipedia.org/wiki/Schottky_diode" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Schottky diodes&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; is just slightly different to Zener/TVS diode&amp;rsquo;s. They commonly have a lower forward voltage than their silicon pendants and are known for their fast speed of switching. That&amp;rsquo;s why apart from rectifier circuits, they are often used in reverse voltage protection circuits. TVS diodes share the quick switching behavior and the low forward voltage, but unlike Schottkys they have a well defined reverse voltage threshold.&lt;/p&gt;&#10;&lt;h3 id="how-to-select-the-correct-tvs-for-your-application"&gt;How to select the correct TVS for your application&lt;/h3&gt;&#10;&lt;p&gt;For DC applications, a unidirectional TVS schould be fine to use. Select the Stand-off voltage of the TVS in a way that it is a little above your normal operating voltage. Then look at the clamping voltage you get further &amp;ldquo;down&amp;rdquo; the line. Is it still below your protected Circuit&amp;rsquo;s &amp;ldquo;Absolute Maximum Ratings&amp;rdquo;? If not, choose a different TVS, make some compromise with the voltage (but don&amp;rsquo;t exceed the breakdown voltage of the TVS), or use another transient suppressor device like a voltage dependent resistor or similar.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-11-02_tvs_diagram.jpg" alt="Typical TVS diode: Voltage/Current diagram"&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Typical TVS diode: Voltage/Current diagram&lt;/span&gt;&lt;a&#10; href="https://www.littelfuse.com/products/tvs-diodes.aspx"&#10; class="attr-link"&#10; aria-label="Attribution 1"&#10; &gt;&#10; &lt;sup class="attr-id"&gt;[1]&lt;/sup&gt;&#10; &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;&#10;</description></item><item><title>Power supervisor MAX1756x on capacitive load</title><link>https://blog.schallbert.de/en/capacitor-problems/</link><pubDate>Tue, 02 Nov 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/capacitor-problems/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-11-02_ic-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: MAX17562 with polarized capacitors on in- and output. This is fatal."&#10; title="Power supervisor MAX1756x on capacitive load" /&gt;&#10;&lt;p&gt;Argh, I killed parts of my &lt;a href="https://blog.schallbert.de/en/pcbs-arrived/"&gt;Power&amp;amp;Preamp circuit for AnywhereAmps Alpha&lt;/a&gt; today. During testing.&lt;/p&gt;&#10;&lt;h2 id="explosion"&gt;Incident&lt;/h2&gt;&#10;&lt;p&gt;I accidently switched VBat and GND on my PCB. You shouldn&amp;rsquo;t think this happens at all, but it did again. The power input/bypass capacitor blew up instantly but my supervisor chip protected the rest of the circuits.&lt;/p&gt;&#10;&lt;h2 id="repair-and-tests"&gt;Repair and tests&lt;/h2&gt;&#10;&lt;p&gt;After repairing with a ceramic capacitor, I continued some tests.&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Reverse polarity test - PASS&lt;/li&gt;&#10;&lt;li&gt;Hard short to GND test - PASS&lt;/li&gt;&#10;&lt;li&gt;Undervoltage lockout - PASS&lt;/li&gt;&#10;&lt;li&gt;Normal voltage retry - &amp;hellip;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="irregularity"&gt;Irregularity&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-11-02_oszi.jpg" alt="Oscilloscope image of an unsuccessful switch-on try @18V, 5A current limit"&gt;&lt;/figure&gt;&#10;But then, when I raised voltage slowly to escape the undervoltage lockout the system wouldn&amp;rsquo;t turn on, but still current consumption went up. To analyze, I connected an oscilloscope to see what was going on. You can see that the power amp&amp;rsquo;s &lt;code&gt;&amp;gt;1400µF&lt;/code&gt; bulk capacitance acts like a soft short to GND so that the power supply voltage drops by &lt;code&gt;2V&lt;/code&gt; within &lt;code&gt;50µs&lt;/code&gt; to the undervoltage lockout trigger level. The supervisor IC then sheds the load, which makes the supply voltage jump back up. I suppose the ringing comes from the power supply&amp;rsquo;s regulator.&lt;/p&gt;&#10;&lt;p&gt;After the blanking time (datasheet MAX1756x) passes, it retries to switch on and the whole process starts over again.&lt;/p&gt;&#10;&lt;h2 id="inductance"&gt;Incident #2&lt;/h2&gt;&#10;&lt;p&gt;When I measured voltage at the VCC rail supplying preamp, power amp, and 9V voltage regulator during unsuccessful switch-on, I noticed that negative voltage spikes occur at around &lt;code&gt;-4V&lt;/code&gt; when the supervisor IC switches off, caused by the inductors in the power amp supply lines.&#10;While I was still wondering that these running resets might not be good, my preamp IC exploded. Hm.&lt;/p&gt;&#10;&lt;h2 id="what-i-learned"&gt;What I learned&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Never use a polarized capacitor as bulk/bypass &amp;ldquo;in front of&amp;rdquo; your protective circuits!&lt;/li&gt;&#10;&lt;li&gt;Use larger-than-100n bypass capacitors before voltage sensing devices when switched loads are capacitive. It helps stabilizing the voltage locally.&lt;/li&gt;&#10;&lt;li&gt;To protect the output side of the switch, add a Schottky or transient voltage suppressor (TVS) diode that clamps potentially harmful voltages&lt;/li&gt;&#10;&lt;li&gt;Limit in-rush current (&lt;a href="https://blog.schallbert.de/en/knowledge-inrushlimiters/"&gt;follow-up article&lt;/a&gt;) as a precaution. I&amp;rsquo;ll switch to &lt;code&gt;MAX17563&lt;/code&gt; for the next circuit revision. It has a programmable current limit so that input voltage doesn&amp;rsquo;t drop too low in the first place.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="protect-from-negative-voltage-spikes"&gt;Protect from negative voltage spikes&lt;/h3&gt;&#10;&lt;p&gt;For my application, a fast-switching clamp diode in Schottky design is enough because behind the MAX1756x IC, the circuit will not see overvoltage spikes but reverse voltage spikes (due to inductors in switching regulator and amp design) only. In &lt;a href="https://blog.schallbert.de/en/knowledge-tvsdiodes/"&gt;this article&lt;/a&gt;, I&amp;rsquo;ll explain the Transient Voltage Suppressors in detail.&lt;/p&gt;&#10;</description></item><item><title>Convert videos with VLC player</title><link>https://blog.schallbert.de/en/video-conversion/</link><pubDate>Wed, 27 Oct 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/video-conversion/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-10-27_vlc-thumb.jpg"&#10; class="post-cover"&#10; alt="VLC media player icon"&#10; title="Convert videos with VLC player" /&gt;&#10;&lt;h2 id="website-media-creation"&gt;Website: media creation&lt;/h2&gt;&#10;&lt;p&gt;For this website, I have been converting media files a lot. You know, the photos I shoot are really big which leads to long page load times and screen-filling images. While it is quite easy to reduce their size, and increase JPEG compression, it is a lot harder to do so with videos.&lt;/p&gt;&#10;&lt;h2 id="the-problem"&gt;The problem&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-10-27_wtf-vlc.jpg" alt="Image: VLC&amp;#39;s preview Effects and filters window"&gt;&lt;/figure&gt;&#10;WTF, VLC?!&#10;I&amp;rsquo;m using the popular &lt;a href="https://www.videolan.org/vlc/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;vlc player&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and somehow I didn&amp;rsquo;t seem to get it right. Either my video doesn&amp;rsquo;t turn out in greyscale as I planned, the output file is empty (Use &lt;code&gt;H.264&lt;/code&gt;, instead of &lt;code&gt;H.265&lt;/code&gt; to fix), the aspect ratio seems distorted, or the video suddenly has rotated which I neither wanted nor expected to happen.&lt;/p&gt;&#10;&lt;h2 id="items-to-cover"&gt;Items to cover&lt;/h2&gt;&#10;&lt;p&gt;This series of steps help me (and maybe you, too) how to make simple video conversions like&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/video-conversion/#colors"&gt;Change or remove colors&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/video-conversion/#rotation"&gt;Rotate a video&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/video-conversion/#cropping"&gt;Crop video, update aspect ratio and resolution&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/video-conversion/#cutting"&gt;Cut / Trim a video&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://blog.schallbert.de/en/video-conversion/#scaling"&gt;Change video resolution / size&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="how-does-the-filter-stuff-work"&gt;How does the filter stuff work?&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-10-27_video-effects.jpg" alt="Image: VLC&amp;#39;s preview Effects and filters window"&gt;&lt;/figure&gt;&#10;&lt;h3 id="preview-filters"&gt;Preview filters&lt;/h3&gt;&#10;&lt;p&gt;VLC has a quick-preview feature for the most common filters in the &lt;code&gt;Tools -&amp;gt; Effects and filters&lt;/code&gt; menu. This directly affects the video you&amp;rsquo;re viewing, but won&amp;rsquo;t affect your saved file. It&amp;rsquo;s just meant for you previewing and controlling the effect properties these filters will have on your video. View &lt;a href="https://www.videoproc.com/troubleshoot-vlc/how-to-crop-a-video-in-vlc.htm" target="_blank" rel="noopener noreferrer" class="external-link"&gt;this VideoProc blog entry&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; for in-depth guides - and - more importantly - many images stepping you through. For the impatient fraction: continue reading 😉&lt;/p&gt;&#10;&lt;h3 id="save-filtered-video-to-file"&gt;Save filtered video to file&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-10-27_profile.jpg" alt="Image: VLC&amp;#39;s conversion profile window for saving"&gt;&lt;/figure&gt;&#10;To save a filter that you selected for your preview, you must add them to the conversion profile. As long as they remain ticked, &lt;em&gt;any&lt;/em&gt; save with this profile will modify your video, eventually even although you unticked all changes you made to the preview filters in the past!&lt;/p&gt;&#10;&lt;h2 id="colors"&gt;How to change colors in my video?&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Open the video file you want to modify.&lt;/li&gt;&#10;&lt;li&gt;Click &lt;code&gt;Tools -&amp;gt; Effects and filters&lt;/code&gt; (or press &lt;code&gt;Ctrl+E&lt;/code&gt;)&lt;/li&gt;&#10;&lt;li&gt;A dialog with three tabs opens. Select the &lt;code&gt;Video Effects&lt;/code&gt; tab.&lt;/li&gt;&#10;&lt;li&gt;Under &lt;code&gt;Essentials&lt;/code&gt;, tick &lt;code&gt;Image Adjust&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Make your changes and hit &lt;code&gt;Save&lt;/code&gt;, then &lt;code&gt;Close&lt;/code&gt;. A preview of the effect is shown in the main window.&lt;/li&gt;&#10;&lt;li&gt;Open the playlist view by pressing &lt;code&gt;Toggle Playlist&lt;/code&gt; (&lt;code&gt;Ctrl+L&lt;/code&gt;)&lt;/li&gt;&#10;&lt;li&gt;Right-click your video, select &lt;code&gt;Save...&lt;/code&gt; in the context menu.&lt;/li&gt;&#10;&lt;li&gt;Press the wrench symbol &lt;code&gt;Edit selected Profile&lt;/code&gt; and in the &lt;code&gt;Video codec&lt;/code&gt; tab, select the &lt;code&gt;Filters&lt;/code&gt; sub-tab.&lt;/li&gt;&#10;&lt;li&gt;Tick the &lt;code&gt;Image properties filter&lt;/code&gt;. This will make your change permanent once you save your file.&lt;/li&gt;&#10;&lt;li&gt;Enter a destination file and hit &lt;code&gt;start&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Don&amp;rsquo;t forget to untick the &lt;code&gt;Image properties filter&lt;/code&gt; in your profile. Also rewind the changes you made in the &lt;code&gt;Video Effects&lt;/code&gt; tab (step 4).&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="rotation"&gt;How to rotate my video?&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Open the video file you want to modify.&lt;/li&gt;&#10;&lt;li&gt;Click &lt;code&gt;Tools -&amp;gt; Effects and filters&lt;/code&gt; (or press &lt;code&gt;Ctrl+E&lt;/code&gt;)&lt;/li&gt;&#10;&lt;li&gt;A dialog with three tabs opens. Select the &lt;code&gt;Video Effects&lt;/code&gt; tab.&lt;/li&gt;&#10;&lt;li&gt;Under &lt;code&gt;Geometry&lt;/code&gt;, tick &lt;code&gt;Transform&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Make your changes and hit &lt;code&gt;Save&lt;/code&gt;, then &lt;code&gt;Close&lt;/code&gt;. A preview of the effect is shown in the main window.&lt;/li&gt;&#10;&lt;li&gt;Open the playlist view by pressing &lt;code&gt;Toggle Playlist&lt;/code&gt; (&lt;code&gt;Ctrl+L&lt;/code&gt;)&lt;/li&gt;&#10;&lt;li&gt;Right-click your video, select &lt;code&gt;Save...&lt;/code&gt; in the context menu.&lt;/li&gt;&#10;&lt;li&gt;Press the wrench symbol &lt;code&gt;Edit selected Profile&lt;/code&gt; and in the &lt;code&gt;Video codec&lt;/code&gt; tab, select the &lt;code&gt;Filters&lt;/code&gt; sub-tab.&lt;/li&gt;&#10;&lt;li&gt;Tick the &lt;code&gt;Video transformation filter&lt;/code&gt;. This will make your change permanent once you save your file.&lt;/li&gt;&#10;&lt;li&gt;Enter a destination file and hit &lt;code&gt;start&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Don&amp;rsquo;t forget to untick the &lt;code&gt;Image properties filter&lt;/code&gt; in your profile. Also rewind the changes you made in the &lt;code&gt;Video Effects&lt;/code&gt; tab.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="cropping"&gt;How to crop my video?&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Open the video file you want to modify.&lt;/li&gt;&#10;&lt;li&gt;Click &lt;code&gt;Tools -&amp;gt; Effects and filters&lt;/code&gt; (or press &lt;code&gt;Ctrl+E&lt;/code&gt;)&lt;/li&gt;&#10;&lt;li&gt;A dialog with three tabs opens. Select the &lt;code&gt;Video Effects&lt;/code&gt; tab.&lt;/li&gt;&#10;&lt;li&gt;Under &lt;code&gt;Crop&lt;/code&gt;, play around with the pixel values and watch the preview window carefully until you&amp;rsquo;re happy with the crop. Note or memorize the settings you&amp;rsquo;d like to apply. Close the dialogue.&lt;/li&gt;&#10;&lt;li&gt;Click &lt;code&gt;Tools -&amp;gt; Preferences&lt;/code&gt; (or press &lt;code&gt;Ctrl+P&lt;/code&gt;)&lt;/li&gt;&#10;&lt;li&gt;On the &lt;code&gt;Interface&lt;/code&gt; (start) Tab, select the radioButton &lt;code&gt;Show Settings - All&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Scroll down to the &lt;code&gt;Video&lt;/code&gt; element and open &lt;code&gt;Filters&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Select the &lt;code&gt;Video cropping filter&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;find the &lt;code&gt;Cropadd&lt;/code&gt; element within the filters list on the left. Enter the values you memorized/ noted down in the preview. Hit &lt;code&gt;Save.&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Open the playlist view by pressing &lt;code&gt;Toggle Playlist&lt;/code&gt; (&lt;code&gt;Ctrl+L&lt;/code&gt;). Then Right-click your video, select &lt;code&gt;Save...&lt;/code&gt; in the context menu.&lt;/li&gt;&#10;&lt;li&gt;Press the wrench symbol &lt;code&gt;Edit selected Profile&lt;/code&gt; and in the &lt;code&gt;Video codec&lt;/code&gt; tab, select the &lt;code&gt;Filters&lt;/code&gt; sub-tab.&lt;/li&gt;&#10;&lt;li&gt;Tick the &lt;code&gt;Video crop filter&lt;/code&gt;. This will make your change permanent once you save your file.&lt;/li&gt;&#10;&lt;li&gt;Enter a destination file and hit &lt;code&gt;start&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Don&amp;rsquo;t forget to untick the &lt;code&gt;Video crop filter&lt;/code&gt; in your profile. Also rewind the changes you made in the &lt;code&gt;Filters&lt;/code&gt; preferences item.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="cutting"&gt;How to trim my video?&lt;/h2&gt;&#10;&lt;p&gt;Note: A more detailed version of this can be found in &lt;a href="https://www.movavi.io/how-to-trim-video-in-vlc-en/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;this post&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Open the video file you want to modify.&lt;/li&gt;&#10;&lt;li&gt;Show Record button in VLC Player (tick &lt;code&gt;View-&amp;gt;Advanced Controls&lt;/code&gt;)&lt;/li&gt;&#10;&lt;li&gt;Jump to where you want the trimmed video to begin.&lt;/li&gt;&#10;&lt;li&gt;Press the red &lt;code&gt;record&lt;/code&gt; button&lt;/li&gt;&#10;&lt;li&gt;Play the video sequence. Stop where you want to end your trim.&lt;/li&gt;&#10;&lt;li&gt;VLC&amp;rsquo;s standard location for video output: &lt;code&gt;C:\Users\Username\Video&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="scaling"&gt;How to change video resolution?&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Open the video file you want to modify.&lt;/li&gt;&#10;&lt;li&gt;Open the playlist view by pressing &lt;code&gt;Toggle Playlist&lt;/code&gt; (&lt;code&gt;Ctrl+L&lt;/code&gt;). Then Right-click your video, select &lt;code&gt;Save...&lt;/code&gt; in the context menu.&lt;/li&gt;&#10;&lt;li&gt;Press the wrench symbol &lt;code&gt;Edit selected Profile&lt;/code&gt; and in the &lt;code&gt;Video codec&lt;/code&gt; tab, select the &lt;code&gt;Resolution&lt;/code&gt; sub-tab.&lt;/li&gt;&#10;&lt;li&gt;Enter the video size you&amp;rsquo;d like to convert to and hit &lt;code&gt;Save&lt;/code&gt;.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Hope this helps!&lt;/p&gt;&#10;</description></item><item><title>My Printed Circuit Boards arrived!</title><link>https://blog.schallbert.de/en/pcbs-arrived/</link><pubDate>Sat, 16 Oct 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/pcbs-arrived/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-10-16_board-thumb.jpg"&#10; class="post-cover"&#10; alt="Freecad example design in 3D view"&#10; title="My Printed Circuit Boards arrived!" /&gt;&#10;&lt;p&gt;Sequel of &lt;a href="https://blog.schallbert.de/en/anywhereamps-get-pcb/"&gt;Printed Circuit Board design blog entry&lt;/a&gt;&lt;/p&gt;&#10;&lt;h2 id="arrival"&gt;Arrival&lt;/h2&gt;&#10;&lt;p&gt;After less than two weeks, my PCBs finally arrived along with all the parts needed to solder. I think they look beautiful :)&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-10-16_board-delivered.jpg" alt="Image: The PCB delivered as-is: green, pads tin-coated"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;h2 id="soldering"&gt;Soldering: Step-by-step&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-10-16_board-partial-test.jpg" alt="Image: cicruit testing"&gt;&lt;/figure&gt;&#10;I decided to solder the PCB in intervals, starting with the power management section. This way, I can test and correct the sections independently from each other. The SOIC (small outline integrated circuit) outline can still be hand-soldered easily when using lots of flux and a tool to &amp;ldquo;zoom in&amp;rdquo; for optical inspection. The only issue was that the IC has an &amp;ldquo;exposed pad&amp;rdquo;, i.e. there&amp;rsquo;s a ground pane on the bottom of the IC for better heat conduction. Soldering this was a nightmare. Finally I managed to fiddle solder through one of the bottom vias while keeping the iron at another via vor nearly a minute until it finally worked out fine.&#10;I&amp;rsquo;m pretty happy with the result, though. The system can be activated pulling *HVEN low, lighting up the LED. Tests for undervoltage/short circuit behavior went very well - the battery pack is protected now.&lt;/p&gt;&#10;&lt;p&gt;The Preamp section was easy to solder as well. Only the polarised capacitors proved to be so big that they covered much of the solder pad - this is a problem because the solder wouldn&amp;rsquo;t want to connect both cap and pad but just stick to one of these.&lt;/p&gt;&#10;&lt;p&gt;The 9V buck converter section - well&amp;hellip; I didn&amp;rsquo;t manage to solder it by hand at all. Quadruple Flat Packs without leads (QFN) are hard enough, but this one has an exposed pad as well and I had to prepare much ground area to get the heat away from this component, so I couldn&amp;rsquo;t even get the temperatues high enough. So I borrowed a Hot Air station, and after some anxious trying and drowning the components in flux, I finally did it.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-10-16_board-inspection.jpg" alt="Image: optical PCB inspection"&gt;&lt;/figure&gt;&#10;I used my digital camera with a macro objective to inspect the solder connections.&lt;/p&gt;&#10;&lt;h2 id="working"&gt;Working&lt;/h2&gt;&#10;&lt;p&gt;I build a lowpass and a highpass into the design to support 2-way speaker systems. To test the three preamp outputs (fullrange, lowpass, highpass), I directly connected the preamp to my speaker. It is not a good idea to do this, because the impedance of the speaker is far lower than the one of the OP-amp, so the output power is really low. Plus, the lower frequencies consume most power so the sound you hear is &amp;ldquo;high-pass&amp;rdquo; already and the low-pass volume is considerably less. Still, it works and I can adjust the gain as intended.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2021-10-16_preamp-audio-demo.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Preamp test&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;That&amp;rsquo;s it, my design is working! And remember, it is open source so you can download the project &lt;a href="https://github.com/Schallbert/AnywhereAmps/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;from my Github repository&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and order your own copy.&lt;/p&gt;&#10;&lt;h2 id="lessons-learned"&gt;Lessons learned&lt;/h2&gt;&#10;&lt;p&gt;Here&amp;rsquo;s the pitfalls and design flaws that should be corrected for Revision2:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Prefer to place all components in the same direction (e.g. horizontally)&lt;/li&gt;&#10;&lt;li&gt;Use thermal reliefs as much as possible to mitigate soldering issues due to thermal conduction&lt;/li&gt;&#10;&lt;li&gt;Exposed pads: When hand soldering, the according solder pads must be extended to the sides of the IC for better solder heat conduction&lt;/li&gt;&#10;&lt;li&gt;Avoid QFN footprints when hand soldering. They are a pain.&lt;/li&gt;&#10;&lt;li&gt;If possible, don&amp;rsquo;t use footprint with &amp;ldquo;exposed pad&amp;rdquo; or &amp;ldquo;power pad&amp;rdquo;. A hot air station might be mandatory.&lt;/li&gt;&#10;&lt;li&gt;Try to leave more space between component pads, esp. if placed horizontally.&lt;/li&gt;&#10;&lt;li&gt;As SMD caps have more height, allow for bigger solder pads.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item><item><title>FreeCAD modeling workflow</title><link>https://blog.schallbert.de/en/freecad-get-started/</link><pubDate>Tue, 12 Oct 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/freecad-get-started/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-10-12_pad-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Freecad example design in 3D view"&#10; title="FreeCAD modeling workflow" /&gt;&#10;&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://upload.wikimedia.org/wikipedia/commons/f/f7/FreeCAD-logo.svg" alt="FreeCad logo"&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;FreeCad logo&lt;/span&gt;&lt;a&#10; href="https://commons.wikimedia.org/wiki/File:FreeCAD-logo.svg#file"&#10; class="attr-link"&#10; aria-label="Attribution 1"&#10; &gt;&#10; &lt;sup class="attr-id"&gt;[1]&lt;/sup&gt;&#10; &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;I just received a second order for AnywhereAmps Alpha. My initial prototype already has a new home, so I&amp;rsquo;ll be building at least three more copies. All the woodworks done by hand would take 4-5h for each copy, so I decided I want to limit the woodworking stuff to the actual building-it-together process that - hopefully - only takes an hour.&#10;I&amp;rsquo;d like to achieve this by having the custom wood parts milled with a computerized numerical control (CNC) machine.&lt;/p&gt;&#10;&lt;h2 id="tools-tools-tools"&gt;Tools, tools, tools&lt;/h2&gt;&#10;&lt;p&gt;Such a machine wouldn&amp;rsquo;t eat the plans I scetched by hand. So I have to familiarize myself with software that is able to transform 2D scetches into a 3D model, and finally another tool to translate the components of the 3D model into paths for the CNC machine to cut my parts.&#10;I chose the &lt;a href="https://www.freecad.org/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;FreeCAD&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; software to do the 3D work.&lt;/p&gt;&#10;&lt;h2 id="every-beginning-is-hard"&gt;Every beginning is hard.&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-10-12_freecad_parts3d.jpg" alt="parts design completed"&gt;&lt;/figure&gt;&#10;But FreeCAD&amp;hellip; well, I&amp;rsquo;ll admit that I&amp;rsquo;m not a CAD designer or mechanical engineer. But still, this tool is hard to get started with. Luckily, there are some quite good tutorials out there like this: &lt;a href="https://www.youtube.com/watch?v=C1JG6l1uIJw" 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; but to be warned, I needed a couple of hours before I had drawn my first part. Plus, what you can see on the image on the right is still a long way before I was done.&lt;/p&gt;&#10;&lt;h2 id="freecad-workflow"&gt;FreeCAD workflow&lt;/h2&gt;&#10;&lt;p&gt;FreeCAD will not tell you how to best work with it. It is a very versatile tool that fulfils many purposes. As I want to use it for drawing 3D parts and/or for &lt;a href="https://en.wikipedia.org/wiki/G-code" target="_blank" rel="noopener noreferrer" class="external-link"&gt;G-code&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; generation, I&amp;rsquo;ll only need a handful of its workbenches.&lt;/p&gt;&#10;&lt;p&gt;Of course, the following lines cannot be more than a quick start guide. There are tons of helpful materials out there like a FreeCad &lt;a href="https://wiki.freecad.org/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;wiki&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, a &lt;a href="https://forum.freecad.org/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;forum&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, and many, many tutorial videos etc. at your hands that explain things better and in more detail than I can ;)&lt;/p&gt;&#10;&lt;h3 id="dimensions-first---spreadsheet-workbench"&gt;Dimensions first - Spreadsheet workbench&lt;/h3&gt;&#10;&lt;p&gt;Let&amp;rsquo;s create a central place where we keep all our project&amp;rsquo;s dimensions. We&amp;rsquo;ll link them in the drawings, 3D models, and assemblies later so that changing values in the spreadsheet will automatically propagate.&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Select the &amp;ldquo;spreadsheets&amp;rdquo; workbench. Create a spreadsheet and rename it e.g. to &lt;code&gt;Dimensions&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Draw a chart with your dimension&amp;rsquo;s names. I found it well readable to have a &lt;code&gt;part_description_attribute&lt;/code&gt; naming convention for every dimension.&lt;/li&gt;&#10;&lt;li&gt;After entering your dimension&amp;rsquo;s value, right-click its cell and select &amp;ldquo;Properties&amp;rdquo;.&lt;/li&gt;&#10;&lt;li&gt;In the following dialogue, select the &amp;ldquo;Alias&amp;rdquo; tab and enter the &lt;code&gt;dimensionName&lt;/code&gt; you entered before&lt;/li&gt;&#10;&lt;li&gt;In any scetch, drawing, parts design or similar workbench, you&amp;rsquo;ll be able to use these dimensions under their Alias name. Click &amp;ldquo;Save&amp;rdquo;.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-10-12_spreadsheet.jpg" alt="Spreadsheet example in FreeCad"&gt;&lt;/figure&gt;&#10;&lt;h3 id="sketch-it---scetcher-in-parts-design-workbench"&gt;Sketch it! - scetcher in Parts Design workbench&lt;/h3&gt;&#10;&lt;p&gt;It&amp;rsquo;s time to create a first sketch of your part using the dimensions you just created. You&amp;rsquo;ll be drawing so-called master scetches fist which contain all elements of a drawing at a specific view, e.g. on the XY-plane.&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Select the &amp;ldquo;Part Design&amp;rdquo; workbench.&lt;/li&gt;&#10;&lt;li&gt;Create a new body using the blue lego-ish symbol.&lt;/li&gt;&#10;&lt;li&gt;Create a new scetch for this body using the scetch symbol. Select e.g. the XY-plane.&lt;/li&gt;&#10;&lt;li&gt;Rename your scetch so it&amp;rsquo;s easy for you to remember its purpose. As your first scetches are the &lt;code&gt;master&lt;/code&gt; ones, I propose to name them &lt;code&gt;master_yourPartName_xy&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Double-click the sketch and start drawing your shapes.&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-10-12_usingalias.jpg" alt="Using spreadsheet alias in Parts Design workbench"&gt;&lt;/figure&gt;&lt;/li&gt;&#10;&lt;li&gt;Make sure you fully constrain (&lt;a href="https://www.youtube.com/watch?v=F6H1iPwVg2o" 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;) your scetch. Use the dimensions you entered in the spreadsheet to help: Double-click your dimensional constraint and type &lt;code&gt;=&lt;/code&gt; or press the tiny round &lt;code&gt;fx&lt;/code&gt; button. Enter the name of your spreadsheet (there is auto-completion, luckily) and then the alias of the dimension you want to constraint to reflect.&lt;/li&gt;&#10;&lt;li&gt;Repeat for any dimensional constraint.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;With your completed master sketches (one each for every dimension), all information is available for any third party service to mill your parts. If you wanted to do that, you could export your sketches to a &lt;code&gt;dxf&lt;/code&gt; file here and you&amp;rsquo;d be done.&#10;Else: please continue reading.&lt;/p&gt;&#10;&lt;h3 id="create-3d-parts---parts-design-workbench"&gt;Create 3D parts - Parts Design workbench&lt;/h3&gt;&#10;&lt;p&gt;To create an actual 3D part from your master sketches, you&amp;rsquo;ll have to create derivative sketches that can be extrudes or cutouts. You&amp;rsquo;ll have to understand FreeCAD&amp;rsquo;s way of tree-aligning parts and scetches first:&#10;Every pad has an underlying scetch that defines how it looks like. Its extruding direction directly depends on the sketch&amp;rsquo;s plane. Each part can have one or more datum planes where other parts can be attached to, again forming pads, holes, or pockets on that specific face.&lt;/p&gt;&#10;&lt;p&gt;FreeCAD will only display the &amp;ldquo;last&amp;rdquo; member of your part&amp;rsquo;s tree, containing all dependant parts you designed before. To toggle visibility, simply hit the space key.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-10-12_pad.jpg" alt="FreeCAD&amp;#39;s pad design tree view"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Create a new sketch in your part&amp;rsquo;s tree structure. This will be your first &lt;code&gt;pad&lt;/code&gt;, i.e. your actual 3D part.&lt;/li&gt;&#10;&lt;li&gt;Choose the &lt;code&gt;create an edge linked to an external geometry&lt;/code&gt; tool (hotkey &lt;code&gt;X&lt;/code&gt;) and select the basic pad of your originating master sketch.&lt;/li&gt;&#10;&lt;li&gt;Draw your shape to match this shape and set its constraints, again using the Dimensions spreadsheet you created earlier. You shouldn&amp;rsquo;t need this too often though, as you linked all possible edges you could find to your master scetch before.&lt;/li&gt;&#10;&lt;li&gt;Now comes the fun part: close the sketch, then hit the &lt;code&gt;pad a selected sketch&lt;/code&gt; icon and enter your dimensions.&lt;/li&gt;&#10;&lt;li&gt;Done!&lt;/li&gt;&#10;&lt;li&gt;To create another pad, a pocket or a hole on top of one your pad&amp;rsquo;s surfaces (called &amp;ldquo;faces&amp;rdquo;), first create a new sketch and name it meaningfully. Then click the face you want this scetch to be based on. Finally, choose the &amp;ldquo;set the Support of a scetch&amp;rdquo; tool and select the scetch you created before. Repeat starting at step 2. until your part is complete.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Hm, your new pad sits right on the master scetch so you cannot see them any more? Don&amp;rsquo;t panic, just continue reading.&lt;/p&gt;&#10;&lt;h3 id="arrange-3d-parts---draft-workbench"&gt;Arrange 3D parts - Draft workbench&lt;/h3&gt;&#10;&lt;p&gt;In my opinion, it&amp;rsquo;s a pain to rearrange parts in the Parts Design workbench. It&amp;rsquo;s getting much easier if you use the &amp;ldquo;Draft&amp;rdquo; workbench for this task. You can clone parts easily here as well and arrange them as you deem fit.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-10-12_move.jpg" alt="Image: move in FreeCAD&amp;#39;s Draft workbench"&gt;&lt;/figure&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Select the &amp;ldquo;Draft&amp;rdquo; workbench&lt;/li&gt;&#10;&lt;li&gt;Choose the pad/part you have been working on before in the parts tree.&lt;/li&gt;&#10;&lt;li&gt;Click the sheep icon to clone your part.&lt;/li&gt;&#10;&lt;li&gt;Make the original pad invisible by hitting space key with the part active.&lt;/li&gt;&#10;&lt;li&gt;Choose the cloned part and rename it meaningfully, e.g. &lt;code&gt;myCoolThing_part&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Select the &amp;ldquo;move&amp;rdquo; icon (marked green in above image).&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-10-12_neat.jpg" alt="Image: part scetches and pads aligned side by side"&gt;&lt;/figure&gt;&#10;&lt;p&gt;A move consists of two steps: first you have to enter the reference location for your move. Either select the point by clicking somewhere in the 3D view and then moveing the cursor a bit (my recommendation) or enter the reference in the fields on the left, confirming each axis with &lt;code&gt;Enter&lt;/code&gt;.&#10;Second, is the actual move vs your reference point. I recommend to enter those values using your keyboard because it&amp;rsquo;s incredibly hard to move to an exact location using the mouse.&#10;Confirm with &lt;code&gt;Enter&lt;/code&gt;, and the move should be complete.&lt;/p&gt;&#10;&lt;p&gt;Cool, you now have a dimensions sheet, master sketches that fully specify your parts, the parts themselves as 3D bodies and all properly that&amp;rsquo;s neatly sorted in your view!&lt;/p&gt;&#10;&lt;h3 id="techdraw-create-a-classic-paper-2d-drawing-as-pdf"&gt;TechDraw: Create a classic &amp;ldquo;Paper 2D&amp;rdquo; drawing as PDF&lt;/h3&gt;&#10;&lt;p&gt;Now, maybe you want to export classic 2D drawings of your part? Luckily, that&amp;rsquo;s an easy task!&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Select the TechDraw workbench&lt;/li&gt;&#10;&lt;li&gt;Press the &lt;code&gt;Insert default page&lt;/code&gt; icon.&lt;/li&gt;&#10;&lt;li&gt;In the parts tree, select the 3D part you&amp;rsquo;d like to have drawn&lt;/li&gt;&#10;&lt;li&gt;Press the &lt;code&gt;Insert multiple views of drawable object(s)&lt;/code&gt; icon and select the views you want to be present.&lt;/li&gt;&#10;&lt;li&gt;Add dimensions, annotations etc using the tool bar.&lt;/li&gt;&#10;&lt;li&gt;Use the &lt;code&gt;turn view frames on/off&lt;/code&gt; if vertices overlap your geometries, so you can select them again.&lt;/li&gt;&#10;&lt;li&gt;Insert cosmetic vertices if you need to, e.g. to draw symmetry lines.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;The resulting document might look like this:&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-10-12_drawing.jpg" alt="Image: classic drawing in FreeCAD"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;p&gt;Further information regarding TechDraw workbench can be found in the &lt;a href="https://wiki.freecad.org/Basic_TechDraw_Tutorial" target="_blank" rel="noopener noreferrer" class="external-link"&gt;FreeCAD Wiki&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;</description></item><item><title>How to create better PCB designs</title><link>https://blog.schallbert.de/en/anywhereamps-get-pcb/</link><pubDate>Tue, 21 Sep 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/anywhereamps-get-pcb/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-10-16_board-delivered-thumb.jpg"&#10; class="post-cover"&#10; alt="Preamp prototype board soldered and working"&#10; title="How to create better PCB designs" /&gt;&#10;&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;&#10;&lt;p&gt;I have my first request for a copy of AnywhereAmps Alpha. I wouldn&amp;rsquo;t want to spend another 10h in building it, so I decided to invest the time into speeding up the manufacturing process. I figured out I could save about an hour per amp if I had the circuit board produced for me. Unfortunately, switching the Circuit Board from hand cutting, milling, drilling, soldering to a PCB design cost me twice as much time compared to a full build already. And I don&amp;rsquo;t even know if this works yet. But let&amp;rsquo;s start at the beginning.&lt;/p&gt;&#10;&lt;h2 id="requirements-on-the-new-pcb"&gt;Requirements on the new PCB&lt;/h2&gt;&#10;&lt;p&gt;The PCB shall contain&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;the preamp circuit as designed for AnywhereAmps Alpha&lt;/li&gt;&#10;&lt;li&gt;the two-way crossover circuits as designed for &lt;em&gt;future&lt;/em&gt; AnywhereAmps Beta&lt;/li&gt;&#10;&lt;li&gt;a switching buck converter to provide 9V DC for the aux effects&lt;/li&gt;&#10;&lt;li&gt;a Power supervisory circuit protecting both battery and PCB from overload, short circuit, under voltage, reverse voltage&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;This way, I want to gradually professionalize the electronics. I intend to use the same PCB for any variant of AnywhereAmps. In addition, I would get rid of some external components that I had to manufacture and place by hand: Fuse, high-current capable power switch, the 9V regulator and its cooling, and some wiring.&lt;/p&gt;&#10;&lt;h2 id="component-selection"&gt;Component selection&lt;/h2&gt;&#10;&lt;h3 id="buck-converter"&gt;Buck Converter&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-09-21_switching_efficiency.jpg" alt="Typical efficiency curve of a buck switching converter vs. load current"&gt;&lt;/figure&gt;&#10;I searched the web for a low-EMI (ElectroMagnetic Interference) switching buck converter with an output of &amp;gt;1A @9V. A linear regulator would have been less noisy, but at a nominal battery voltage of 18V I would have to cope with &amp;gt;9W of thermal losses worst case. Plus, this solution wouldn&amp;rsquo;t have been battery-saving at all. I selected a (costy) design that has all necessary components in-a-box so I just had to get the PCB layout right for EMI reduction. Add a couple of bypass capacitors and a resistor divider for the target voltage setpoint. Done! (I thought at that time. &lt;a href="https://blog.schallbert.de/en/pcbs-arrived/#soldering"&gt;Here&amp;rsquo;s how it actually turned out&lt;/a&gt;)&lt;/p&gt;&#10;&lt;h3 id="power-supervisor"&gt;Power Supervisor&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-09-21_converter.jpg" alt="Integrated Buck Converter: Almost a cube"&gt;&lt;/figure&gt;&#10;The only protection for my amps was a fuse until now, its value set to the highest &amp;ldquo;normal&amp;rdquo; current I expected in the circuits. In the early prototype phases I killed some opamps by accidentally applying a reverse voltage. And I want to avoid to totally drain the battery pack when I forget to look at the charge indicators. So I selected a &lt;a href="https://blog.schallbert.de/en/configure-max1756x/"&gt;power supervisor IC&lt;/a&gt; that would detect any undervoltage or fault condition and protect both circuits and battery from damage.&lt;/p&gt;&#10;&lt;h3 id="passive-components-grouping"&gt;Passive components grouping&lt;/h3&gt;&#10;&lt;p&gt;I learned that manufacturing a PCB with many different parts would be more expensive and more prone to errors than using less different parts, even if the total number of parts increased this way. So I redesigned many values and re-did the simulation for the audio preamp with these new values. Finally, I was able to break it down from 8/5 to 3 different resistor and 3 different capacitor values!&lt;/p&gt;&#10;&lt;h3 id="capacitors-again"&gt;Capacitors, again&lt;/h3&gt;&#10;&lt;p&gt;I first used MLCC (Multilayer ceramic capacitors) in my design, both for bypass, and for the audio decoupling capacitors. After again reading the documents I pointed out in my &lt;a href="https://blog.schallbert.de/en/preamp-design-considerations/"&gt;preamp design considerations post&lt;/a&gt;, I realized that this capacitor design seems to have trouble with distortion on audio signals. The image below shows a comparison of different capacitor types and sizes. You see that film capacitors have the best performance and MLCCs are worst. As I wasn&amp;rsquo;t able to find film capacitors with the size I needed on a SMD footprint, and aluminium electrolytic caps were too large, I selected polarized tantalum capacitors. For the bypass uses I selected &lt;a href="https://passive-components.eu/why-low-esr-matters-in-capacitor-design/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;low-ESR&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; types.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-09-21_capcompare.jpg" alt="capacitor types compared for Audio usage"&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;TI Audio Design Journal: Capacitor types compared&lt;/span&gt;&lt;a&#10; href="https://www.ti.com/lit/an/slyt796/slyt796.pdf?ts=1630627717321&amp;amp;ref_url=https%253A%252F%252Fwww.startpage.com%252F"&#10; class="attr-link"&#10; aria-label="Attribution 1"&#10; &gt;&#10; &lt;sup class="attr-id"&gt;[1]&lt;/sup&gt;&#10; &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;h2 id="circuit-design"&gt;Circuit design&lt;/h2&gt;&#10;&lt;p&gt;I chose the open-source software &lt;a href="https://www.kicad.org/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;KiCad&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; as an Electronic Design Automation tool and redrew all my existing circuit plans, adding the components described above. Look how the amplifier section only fills the right half of the schematics while the left half is devoted to power supply and supervisory circuits.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-09-21_schematics.jpg" alt="AnywhereAmps Power &amp;amp; Preamp circuit with KiCad"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;p&gt;I tried to get the schematics as neat as possible to make them easy to read. I used global labels a lot and added connectors for simplified mounting.&lt;/p&gt;&#10;&lt;h2 id="pcb-design"&gt;PCB design&lt;/h2&gt;&#10;&lt;p&gt;This was the hardest part for me. I never designed a PCB for audio usage before, so I had to read a lot of guidelines before I started.&lt;/p&gt;&#10;&lt;h3 id="how-tos-and-tutorials"&gt;How tos and tutorials&lt;/h3&gt;&#10;&lt;p&gt;I list some of them below for your reference:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://www.allaboutcircuits.com/technical-articles/clean-power-for-every-ic-part-1-understanding-bypass-capacitors/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;noise on PCBs and how to avoid&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://www.renesas.com/en/document/apn/an1325-choosing-and-using-bypass-capacitors" target="_blank" rel="noopener noreferrer" class="external-link"&gt;how to best place bypass capacitors&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://www.autodesk.com/products/eagle/blog/8-pcb-grounding-rules/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ground plane or not?&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://electronics.stackexchange.com/questions/52603/how-to-connect-ground-planes-together" target="_blank" rel="noopener noreferrer" class="external-link"&gt;how to handle different grounds for signals, power, etc.&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://resources.altium.com/p/top-5-pcb-design-guidelines-every-pcb-designer-needs-know" target="_blank" rel="noopener noreferrer" class="external-link"&gt;how to optimize parts placement for soldering&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://www.elektronikpraxis.vogel.de/pcb-design-regeln-sieben-suenden-beim-leiterplatten-design-a-356703/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;avoid these pitfalls when designing PCBs&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://www.multi-circuit-boards.eu/leiterplatten-design-hilfe/oberflaeche/leiterbahn-strombelastbarkeit.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;how much current can my track handle with this width?&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://community.aisler.net/c/knowledge-base/design-rules/41" target="_blank" rel="noopener noreferrer" class="external-link"&gt;some PCB design specifications for manufacturability&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&#10;I tried to apply those design guidelines the best I could.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="anywhereamps-pcb-explained"&gt;AnywhereAmps PCB explained&lt;/h3&gt;&#10;&lt;p&gt;The result after a couple of evenings:&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-09-21_pcb.jpg" alt="The PCB fully routed on a 2-layer design"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;p&gt;I separated the PCB into three zones.&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Left: the 9V switching regulator, following its reference design (datasheet) it would create a current circle around itself using the bypass capacitors and cancel out most of the EMI. Look at the thermal design, it has a lot of copper and quite some vias to get the heat conducted away from the die.&lt;/li&gt;&#10;&lt;li&gt;Center: The power terminals and supervisory circuit. I placed solder jumpers to select the nominal battery voltage for the undervoltage detection.&lt;/li&gt;&#10;&lt;li&gt;Right and top: Preamp and analog signal circuitry, separated by the other electronics stuff using a thick ground track on the top copper and an own ground plane on the bottom. I hope this is enough&amp;hellip;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;As I&amp;rsquo;ll hand-solder the first prototypes to check if it all works OK, I used &amp;ldquo;big&amp;rdquo; 0805 SMD components. I increased the space in between them by more than the PCB specification would demand. Unfortunately, the highly integrated power ICs were not available in &amp;ldquo;hobby soldering electronics&amp;rdquo; sizes so I&amp;rsquo;ll have to go through some pain there :(&lt;/p&gt;&#10;&lt;p&gt;So this is how it will look like. The PCB size will be &lt;code&gt;45mm x 69mm&lt;/code&gt;.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-09-21_pcb3d.jpg" alt="3D view of the PCB"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;h2 id="contribute-or-download"&gt;Contribute or download&lt;/h2&gt;&#10;&lt;p&gt;All the CAD models and schematics are open source and available for download and your contributions on my &lt;a href="https://github.com/Schallbert/AnywhereAmps/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;dedicated Github repository&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Up next: &lt;a href="https://blog.schallbert.de/en/pcbs-arrived/"&gt;PCB delivered and soldered&lt;/a&gt;&lt;/p&gt;&#10;</description></item><item><title>Preamp design considerations</title><link>https://blog.schallbert.de/en/preamp-design-considerations/</link><pubDate>Wed, 25 Aug 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/preamp-design-considerations/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/circuit_board_soldered-thumb.jpg"&#10; class="post-cover"&#10; alt="Preamp prototype board soldered and working"&#10; title="Preamp design considerations" /&gt;&#10;&lt;h2 id="decoupling-capacitor-selection"&gt;Decoupling capacitor selection&lt;/h2&gt;&#10;&lt;p&gt;Some tech deep-dive background information about how I selected components for my preamp circuit board.&lt;/p&gt;&#10;&lt;h3 id="why-decoupling-is-needed"&gt;Why decoupling is needed&lt;/h3&gt;&#10;&lt;p&gt;By decoupling, I specifically mean direct current (DC) decoupling in this case.&#10;The preamp needs to be decoupled both from the source (the musical instrument) and the sink (the power amplifier).&#10;This has multiple reasons:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;the supply voltages of the three partners may differ (e.g. 9V guitar phantom, 18V preamp, 52V power amp) and I don&amp;rsquo;t want to accidentally short-circuit one of them&lt;/li&gt;&#10;&lt;li&gt;Output impedance, I mean the load capacity of the output stage might be limited. If I connect a low input resistance e.g. directly to a guitar&amp;rsquo;s pickup, I might not get any signal at all because the source is too weak to power it correctly.&lt;/li&gt;&#10;&lt;li&gt;Noise reduction through electric coupling&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="why-capacitors-can-do-the-job"&gt;Why capacitors can do the job&lt;/h3&gt;&#10;&lt;p&gt;There are multiple ways of electrically decoupling circuits. You can use a &lt;a href="https://en.wikipedia.org/wiki/Opto-isolator" target="_blank" rel="noopener noreferrer" class="external-link"&gt;photocoupler&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; to totally (i.e. galvanically) separate two circuits from each other while only the signal is passed between the circuits, or - in my case - a capacitor to only let alternating currents pass, and to block constant current altogether.&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;How does this work?&lt;/em&gt;, you ask?&#10;Well, a capacitor acts a bit like a water tank. If you apply a constant voltage (pressure), the tank will flood to a certain degree. When this level is reached, there&amp;rsquo;s no flow (current) anymore, neither in nor out. But if you now vary the voltage around this static level (water pressure variation), you will measure some medium (current) flowing in and out of the tank again.&lt;/p&gt;&#10;&lt;p&gt;It&amp;rsquo;s the same with the capacitor. If you connect it in series to a sink, it will only pass current over when the input voltage level is changing.&lt;/p&gt;&#10;&lt;h2 id="decoupling-capacitor-type"&gt;Decoupling Capacitor type&lt;/h2&gt;&#10;&lt;p&gt;Multilayer Ceramic, Polypropylene, Electrolytic&amp;hellip;? After reading &lt;a href="https://www.ti.com/lit/an/slyt796/slyt796.pdf?ts=1630627717321&amp;amp;ref_url=https%253A%252F%252Fwww.startpage.com%252F" target="_blank" rel="noopener noreferrer" class="external-link"&gt;this guide&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; from Texas Instruments, I understood this document is all about distortion. Distortion would hit the hardest on the instrument input before the preamp, because this will be amplified by both amp stages. When using an electrolytic capacitor, there should be a voltage bias applied to reduce distortion in the low frequency range. With my design, the bias voltage is &lt;code&gt;1/2&lt;/code&gt; of &lt;code&gt;VCC&lt;/code&gt;, so even higher than the &lt;code&gt;5V&lt;/code&gt; used in the guide - I think, I&amp;rsquo;ll be fine with a &amp;ldquo;cheap&amp;rdquo; polarized Aluminium cap on the &lt;a href="https://en.wikipedia.org/wiki/Total_harmonic_distortion" target="_blank" rel="noopener noreferrer" class="external-link"&gt;THD+n&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; side.&lt;/p&gt;&#10;&lt;p&gt;There&amp;rsquo;s another thing about MLCCs I noticed myself: With loudspeakers disconnected at high preamp gain, the power amps I&amp;rsquo;m using still play music! How&amp;rsquo;s that? The power amp board is using MLCCs which act like piezo actuators. Their vibration is amplified by the PCB they are mounted on which gives a proper sound level. Crazy. For more geeky details, have a look at &lt;a href="https://www.edn.com/power-tip-49-avoid-these-common-mlcc-pitfalls/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;EDN&amp;rsquo;s blog post&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; or - again a &lt;a href="https://e2e.ti.com/blogs_/b/powerhouse/posts/how-to-reduce-acoustic-noise-of-mlccs-in-power-applications" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Texas Instruments guide&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="capacitor-size"&gt;Capacitor size&lt;/h3&gt;&#10;&lt;p&gt;To judge capacitor size, I had to take a couple of aspects into account:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;A bigger capacitor will lower my bottom end cutoff frequency&lt;/li&gt;&#10;&lt;li&gt;A bigger capacitor will put more load on the input source due to charge losses&lt;/li&gt;&#10;&lt;li&gt;A bigger capacitor will cost more, especially the high-quality ones.&lt;/li&gt;&#10;&lt;li&gt;A bigger capacitor might have a larger footprint.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;I wanted a lower cutoff of &lt;code&gt;&amp;lt;20Hz&lt;/code&gt; for full HiFi capability. As that&amp;rsquo;s the &lt;code&gt;-3dB&lt;/code&gt; point already, I chose it to be even lower at &lt;code&gt;15Hz&lt;/code&gt; calculated via \(f=\frac{1}{2 \pi RC}\). With an input impedance of &lt;code&gt;10kOhm&lt;/code&gt; (the value of the resistor clamping to virtual ground) I yield a target input capacitor size of &lt;code&gt;1µF&lt;/code&gt;.&#10;With this capacitor, the input source will be considerably more affected by the &lt;code&gt;10kOhms&lt;/code&gt; input resistor than by the capacitor with its low series resistance (&lt;a href="https://passive-components.eu/why-low-esr-matters-in-capacitor-design/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ESR&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;) of some hundred mOhms, so the chosen value is fine.&lt;/p&gt;&#10;&lt;p&gt;For the output capacitor, the calculation is similar. The power stage has an input impedance of &lt;code&gt;20kOhm&lt;/code&gt;, so the lower cutoff frequency of &lt;code&gt;17Hz&lt;/code&gt; is reached wich a &lt;code&gt;0.47uF&lt;/code&gt; capacitor already. Anyways, to keep value differences at bay, I decided to go with the &lt;code&gt;1µF&lt;/code&gt; as well.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://blog.schallbert.de/en/projects/aa_alpha/"&gt;Back to Project main page&lt;/a&gt;&lt;/p&gt;&#10;</description></item><item><title>Shelly13 - Part2</title><link>https://blog.schallbert.de/en/thoughts-about-shelly/</link><pubDate>Fri, 09 Jul 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/thoughts-about-shelly/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-07-09_shellydraw-thumb.jpg"&#10; class="post-cover"&#10; alt="shelly13&amp;#39;s folding mechanism"&#10; title="Shelly13 - Part2" /&gt;&#10;&lt;h2 id="shelly13-project-status-prototype-scetches"&gt;Shelly13: Project status, prototype scetches&lt;/h2&gt;&#10;&lt;p&gt;I&amp;rsquo;m still thinking a lot about &lt;a href="https://blog.schallbert.de/en/shelly13/"&gt;Shelly13&lt;/a&gt; and how it can possibly work out to have a bigger speaker, a homogenous sound with some punch, and a sealed cabinet design that can be folded.&lt;/p&gt;&#10;&lt;h3 id="cabinet-sizing-and-volume"&gt;Cabinet: Sizing and Volume&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/shelly13/scetch_unfolded.jpg" alt="Cabinet, inside view scetch"&gt;&lt;/figure&gt; Latest designs in my head work with 12x10 and a 14x10 inch shells. It will look a bit like a cake when unfolded, with a total cabinet volume of around 38l incl. speakers and amplifier mounted inside. It will be about 75cm of total height with its feet unfolded and about 29cm high when completely folded.&lt;/p&gt;&#10;&lt;h3 id="speakers"&gt;Speaker(s)&lt;/h3&gt;&#10;&lt;p&gt;While my first design shows a 10&amp;quot; speaker mounted at the front of the cabinet, I&amp;rsquo;ll most likely switch to a two-way design for simplified mounting, mitigating the issues about milling a huge curved surface and creating an adapter plate. So I&amp;rsquo;ll chose an 8&amp;quot; Bass speaker in a downfire setup and a 3&amp;quot; mid-high speaker at the front.&lt;/p&gt;&#10;&lt;h3 id="amplification"&gt;Amplification&lt;/h3&gt;&#10;&lt;p&gt;I&amp;rsquo;m thinking about using a car amplifier that takes its signal from an active crossover with separated outputs for low and high frequencies, creating a bi-amp design with bridged terminals.&lt;/p&gt;&#10;&lt;h3 id="folding-mechanism"&gt;Folding mechanism&lt;/h3&gt;&#10;&lt;p&gt;Folding will take place with the help of drawer rails mounted in between the shells. It is crucial that the cabinet is sealed air-tight when unfolded, so there has to be a ring gasket that engages when the lower wooden ring at the bottom of the 12&amp;quot; shell meets the top ring of the 14&amp;quot; shell. &lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/shelly13/scetch_top_rings.jpg" alt="Cabinet ring system scetch"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;h2 id="nah-this-is-too-complex"&gt;Nah, this is too complex!&lt;/h2&gt;&#10;&lt;p&gt;Ok, let&amp;rsquo;s strip it down. Leave the foldability away for the initial prototype. Reduce cabinet size instead. Concentrate on electronics and manufacurability first.&#10;Outcome of this scope change is: &lt;a href="https://blog.schallbert.de/en/projects/aa_alpha/"&gt;AnywhereAmps Alpha&lt;/a&gt;!&lt;/p&gt;&#10;</description></item><item><title>Shelly13 - Part1</title><link>https://blog.schallbert.de/en/shelly13/</link><pubDate>Mon, 28 Jun 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/shelly13/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/shelly13/shelly13-thumb.jpg"&#10; class="post-cover"&#10; alt="shelly13 prototype in CAD"&#10; title="Shelly13 - Part1" /&gt;&#10;&lt;p&gt;After playing &lt;a href="https://blog.schallbert.de/en/projects/mobfobamp/"&gt;MobFobAmp&lt;/a&gt; for a while, I wanted to take another step in the evolution towards a really good portable instrument combo. With this design I&amp;rsquo;m trying to address the shortcomings of the previous prototypes:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Power output could be higher&lt;/li&gt;&#10;&lt;li&gt;More volume in the low lows would be nice&lt;/li&gt;&#10;&lt;li&gt;Folding mechanism a bit fiddly.&lt;/li&gt;&#10;&lt;li&gt;I&amp;rsquo;d like a one-step unfold, preferably one-handed operation possible&lt;/li&gt;&#10;&lt;li&gt;Mechanical stability and robustness could be improved&lt;/li&gt;&#10;&lt;li&gt;Sensitive components mounted on the outside requiring to be handled with care&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="a-first-design"&gt;A first design&lt;/h2&gt;&#10;&lt;p&gt;Taking the above points into account, I figured out that I had to compromise foldability a bit to get all the sensitive components including the speaker chassis to the inside of the cabinet. A ball-shaped design would be ideal for best low frequency / size ratio, but I didn&amp;rsquo;t know how to manufacture such a design - so I went with a cylindrical approach.&#10;I&amp;rsquo;d like the speaker to &amp;ldquo;pop up&amp;rdquo; so it would be protected from mechanical damage when folded.&#10;So my first idea looks like this:&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/shelly13/shelly13.jpg" alt="Shelly13 scetch"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;h2 id="project-constraints"&gt;Project constraints&lt;/h2&gt;&#10;&lt;p&gt;I cannot manufacture the cylinders myself so I&amp;rsquo;d have to contact a drums manufacturer to send some unfinished custom-size Tom-toms over to me. Even if I get those, I&amp;rsquo;ll have to cut on a curved surface which I never did before, and I&amp;rsquo;d require some 3D printing / routing to craft an adaptor plate between speaker and cylinder&amp;hellip;&#10;Or will I just have the speaker mounted on the top/bottom?&#10;Plus, I&amp;rsquo;d have to think about how to include a more powerful amplifier into my design - there are some ideas like &lt;a href="https://pioneer-car.eu/gm-d1004/gm-d1004" target="_blank" rel="noopener noreferrer" class="external-link"&gt;this Pioneer car amp&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; or the specialized Hi-Fi amp from &lt;a href="https://www.hypexdirect.com/products/modules/amplifier-modules/ucd180hg-with-hxr" target="_blank" rel="noopener noreferrer" class="external-link"&gt;DIY Class D&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; but they have very different and maybe harsh requirements on power supply&amp;hellip;&lt;/p&gt;&#10;&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;&#10;&lt;p&gt;You see, many open items to think about.&#10;I&amp;rsquo;ll let this sink in for some time before continuing work on this project.&lt;/p&gt;&#10;</description></item><item><title>Paginator broken, Favorite icon placed</title><link>https://blog.schallbert.de/en/jekyll-icon-paginator/</link><pubDate>Sun, 13 Jun 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/jekyll-icon-paginator/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/favicon-thumb.jpg"&#10; class="post-cover"&#10; alt="favicon implementation on a website"&#10; title="Paginator broken, Favorite icon placed" /&gt;&#10;&lt;h3 id="adding-a-tab-icon---favicon"&gt;Adding a &amp;ldquo;tab&amp;rdquo; icon - favicon&lt;/h3&gt;&#10;&lt;p&gt;I was worrying about a strange error message that Jekyll threw at any page reload since some pushes: &lt;code&gt;[2021-06-13 22:07:43] ERROR '/favicon.ico' not found.&lt;/code&gt; Some online research revealed that this is neither a Jekyll, nor a Theme error, but a missing file error. See &lt;a href="https://blog.schallbert.de/en/projects/thissite/#favicon"&gt;here&lt;/a&gt; how I implemented the icon for this page.&lt;/p&gt;&#10;&lt;h3 id="pagination-again"&gt;Pagination, again&lt;/h3&gt;&#10;&lt;p&gt;I found out that somehow, the pagination doesn&amp;rsquo;t even work on my page while the layouting assumes that it&amp;rsquo;s preparing the pages. Now, if I try to access the page by pressing the &amp;ldquo;previous&amp;rdquo; button, I get a 404 error. Till now, I wasn&amp;rsquo;t able to fix this because the pagination fails silently and I do not know where.&#10;Switching it off completely for now.&lt;/p&gt;&#10;</description></item><item><title>MobFobAmp</title><link>https://blog.schallbert.de/en/mobfobamp-diy/</link><pubDate>Thu, 10 Jun 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/mobfobamp-diy/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/unfolded-thumb.jpg"&#10; class="post-cover"&#10; alt="MobFobAmp Folded"&#10; title="MobFobAmp" /&gt;&#10;&lt;h2 id="plans"&gt;Plans&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&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-0"&#10; aria-label="Image: MobFobAmp pencil drawing on a sheet of paper. It features front and side view, and includes a scetch of the speaker chassis which is mounted inside-out. The box itself looks like a rectangle with hinges that allow it to fold flat."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/mobfobamp/scetch_sidefront.jpg"&#10; alt="Image: MobFobAmp pencil drawing on a sheet of paper. It features front and side view, and includes a scetch of the speaker chassis which is mounted inside-out. The box itself looks like a rectangle with hinges that allow it to fold flat."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-1"&#10; aria-label="Image: MobFobAmp pencil drawing on a sheet of paper. It is a top/bottom view, and includes domensions. It looks like a square ."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/mobfobamp/scetch_topbottom.jpg"&#10; alt="Image: MobFobAmp pencil drawing on a sheet of paper. It is a top/bottom view, and includes domensions. It looks like a square ."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-2"&#10; aria-label="Image: MobFobAmp&amp;#39;s circuit diagram as a pencil drawing. It shows a battery, fuse, on/off switch, status indicator LED the amplifier board with in- and output jacks and the speaker it drives."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/mobfobamp/scetch_circuitdiagram.jpg"&#10; alt="Image: MobFobAmp&amp;#39;s circuit diagram as a pencil drawing. It shows a battery, fuse, on/off switch, status indicator LED the amplifier board with in- and output jacks and the speaker it drives."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;/div&gt;&lt;div class="hugo-gallery__caption"&gt;&#10; Plans for my Mobile Foldable Amplifier.&#10; &lt;/div&gt;&lt;/div&gt;&#10;&#10; &lt;div class="hugo-gallery__full" aria-live="polite"&gt;&lt;figure&#10; id="gallery-full-0"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/mobfobamp/scetch_sidefront.jpg"&#10; alt="Image: MobFobAmp pencil drawing on a sheet of paper. It features front and side view, and includes a scetch of the speaker chassis which is mounted inside-out. The box itself looks like a rectangle with hinges that allow it to fold flat."&gt;&lt;figcaption&gt;Front and side view scetch with dimensions&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-1"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/mobfobamp/scetch_topbottom.jpg"&#10; alt="Image: MobFobAmp pencil drawing on a sheet of paper. It is a top/bottom view, and includes domensions. It looks like a square ."&gt;&lt;figcaption&gt;Top and bottom view scetch with dimensions, shows the mounting side of the panels&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-2"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/mobfobamp/scetch_circuitdiagram.jpg"&#10; alt="Image: MobFobAmp&amp;#39;s circuit diagram as a pencil drawing. It shows a battery, fuse, on/off switch, status indicator LED the amplifier board with in- and output jacks and the speaker it drives."&gt;&lt;figcaption&gt;Minimal added circuitry: Switch, fuse, LED, 1k Resistor to set amp gain to max&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&#10;&lt;/section&gt;&#10;&#10;&lt;h2 id="lists"&gt;Lists&lt;/h2&gt;&#10;&lt;h3 id="tools"&gt;Tools&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;Required tools&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;Power drill&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;Router, copy ring, rip fence&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;Multi-tool: Sanding, Wood cut, cutter head, compasses&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;Optional: hand-held circular saw with guard rail&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;Hand saw, fine&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;Clamps&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;Chisel, 12mm&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;Hammer&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;Screwdrivers, bits&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;Wood drills: 3mm, 5mm, 6mm, 8mm, 12mm, counterbore 45°, Forstner 30mm&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;Steel drills: 3mm, counterbore 45°&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;Cutter heads: 8mm straight, 12mm straight, 45° optional&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;Soldering iron, solder, shrinking tube, 1m of 0.75mm² cable etc.&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="material-list"&gt;Material list&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;Item&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Size [mm]&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Count&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;~ Cost [€]&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;Front/Back: Birch Multiplex&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;L 500 B 260 W 12&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;12&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;Top/Bottom: Birch Multiplex&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;L 260 B 248 W 12&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;4&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;Sides: Birch Multiplex&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;L 500 B 110 W 12&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;8&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;Strengthening &amp;amp; Mounts&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;L 260 B 236 W 12&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;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;Screws (preferably Torx)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;12 x 3&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;100&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5&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;Screws (Battery holder, handlebar)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;35 x 4&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;0.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;Screws &amp;amp; nuts (Speaker)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;15 x M4&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;0.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;Board holder&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;d5 x 16&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;14&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;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Piano hinge&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;500 x 32&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;6&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;18&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;Latches&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;50 x 12&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;9&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;Bicycle inner tube (Flexible hinges &amp;amp; Straps)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;28&amp;quot;&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;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;&amp;ldquo;P-Type&amp;rdquo; Foam or Rubber seal/ gasket&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;500 x 9 x 7&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;6&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;8&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;&amp;ldquo;P-Type&amp;rdquo; Foam or Rubber seal/ gasket&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;260 x 9 x 7&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;4&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;&amp;ldquo;P-Type&amp;rdquo; Foam or Rubber seal/ gasket&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;236 x 9 x 7&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;4&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;&amp;ldquo;I-Type&amp;rdquo; Foam Seal (Speaker)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;600 x 9 x 2&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;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;Aluminium bar&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;260 x 12&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;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;Spacer (Auto-unfold feature)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;do 30 di 4&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;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;Handlebar&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;What you prefer&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;?&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;Amplifier (e.g. Adafruit 20W Class-D amp)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&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;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;Speaker (e.g. Visaton W 170S 4Ohm)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;d170, W 81&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;35&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;Battery, 10V - 14V&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;What you prefer&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;?&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;Fuse, switch, LED, resistors: See Circuit diagram&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;What you prefer&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;?&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;Spare wood e.g. as mask&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;e.g. 300 x 300&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;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;Gum glue or hot melt (airtight gaps)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;20g&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;3&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h2 id="the-build"&gt;The Build&lt;/h2&gt;&#10;&lt;h3 id="routing"&gt;Routing&lt;/h3&gt;&#10;&lt;p&gt;Let&amp;rsquo;s first mill all parts so the gaskets for sealing the cabinet can be mounted.&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/scetch_routerripfence_7mm.png" alt="router_ripfence_7mm"&gt;&lt;/figure&gt;Prepare your router with the &lt;code&gt;8mm&lt;/code&gt; straight cutter head &lt;code&gt;@3mm&lt;/code&gt; cut depth, and add the rip fence so that the cutter&amp;rsquo;s center is at &lt;code&gt;7mm&lt;/code&gt; distance from the fence. Make sure to control the router thoroughly so your cuts are straight and clean.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Continue with the front and back parts, &lt;code&gt;500 x 260 x 12&lt;/code&gt;. Mill the long outer sides of the two boards.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Now cut the foldable sides of the cabinet. Take two of the four &lt;code&gt;500 x 110 x 12&lt;/code&gt; pieces. Make the long sides face upwards. Use your router to cut the inner sides of these boards like so:&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/scetch_bevelsides_8mm.png" alt="bevel_sides_8mm"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Now it&amp;rsquo;s time to mill the surrounding gaskets of the top/ bottom parts - mill all 4 of the inner sides. Refer to &lt;a href="https://blog.schallbert.de/en/mobfobamp-diy/#plans"&gt;scetches&lt;/a&gt; for help.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;As we&amp;rsquo;re at it, exchange the cutting head by the &lt;code&gt;12mm&lt;/code&gt; one. Re-adjust the rip fence so the router will mill with no distance to the fence. Cut the upper &amp;ldquo;rear&amp;rdquo; end of the top/ bottom part so that the rubber hinge can be mounted here later.&#10;If there are strands like you can see on the image below, gently sand them down with an 180 grain paper.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/rubberhinge_cutout.jpg" alt="cutout"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Cut the hole for the speaker in the front part. Use the compasses tool attached to the router or the multi tool to cut the inner ring of &lt;code&gt;148mm&lt;/code&gt; diameter. Three or four steps of router depth variation should be OK.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Mill a mask with the targeted outer speaker diameter into a spare piece of wood, e.g. &lt;em&gt;height-of-your-copy-ring [mm]&lt;/em&gt; multiplex. As you will be using the copy ring to make the bevel, accomodate for its width so you will reach a &lt;code&gt;170mm&lt;/code&gt; final diameter. It should look like this now:&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/speakercutout_mask.jpg" alt="speaker_cutout"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Center the mask on the &lt;code&gt;148mm&lt;/code&gt; cutout at the front and atttach it (e.g. use double-sided tape). Mount the copy ring to the router. Make sure you use the &lt;code&gt;12mm&lt;/code&gt; cutter head and target a bevel depth of no more than &lt;code&gt;8mm&lt;/code&gt; - The speaker has &lt;code&gt;7mm&lt;/code&gt; depth, add &lt;code&gt;1mm&lt;/code&gt; for the gasket.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Sand the edges a little. Use the multi tool to correct small bumps that might still be left. Repeat until the speaker fits in snugly.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/speakercutout_ready.jpg" alt="speaker_cutout_complete"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Now cut the notches for the Latches&amp;rsquo; counterparts. Set the router to a depth of material + gasket grove depth (so it can sit behind the gasket) and used the rip fence to match the center of the top and bottom parts. Note that you&amp;rsquo;ll cut orthogonally through the groves for the gaskets you made previously.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/latch_counterpart.jpg" alt="latch_counterpart"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="hardware"&gt;Hardware&lt;/h3&gt;&#10;&lt;p&gt;The hardware is mounted before assembly of the cabinet as the gaskets&amp;rsquo;s counter pressure will increase difficulty of getting the cabinet shut tight.&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Mount the latches&amp;rsquo; counterparts at the top and bottom boards with two &lt;code&gt;3x12&lt;/code&gt; screws each.&lt;/li&gt;&#10;&lt;li&gt;Use hot melt or gum glue to seal the metal&amp;rsquo;s edges against the cutout.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/latch_counterpart_sealed.jpg" alt="latch_counterpart_sealed"&gt;&lt;/figure&gt;&lt;/li&gt;&#10;&lt;li&gt;Cut a piece of &lt;code&gt;260mm&lt;/code&gt; length from the bicycle inner tube.&lt;/li&gt;&#10;&lt;li&gt;Cut this piece in half parallel to the long side, so you get two straps of &lt;code&gt;260mm&lt;/code&gt; length.&lt;/li&gt;&#10;&lt;li&gt;Take the aluminium bars. Cut 5 holes equidistantly into each bar, use the &lt;code&gt;45°&lt;/code&gt; counterbore so the countersunk heads will fit.&lt;/li&gt;&#10;&lt;li&gt;Put the strap of the tube below the aluminium bar and mount them at the top rear of the top board.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/rubberhinge_mounted.jpg" alt="rubberhinge_mounted"&gt;&lt;/figure&gt;&lt;/li&gt;&#10;&lt;li&gt;Repeat step 6 with the bottom board.&lt;/li&gt;&#10;&lt;li&gt;Put Front, Top, and Bottom boards together on the table so they form a big C.&lt;/li&gt;&#10;&lt;li&gt;Mount the latches centered to the front board, make sure they keep the top / bottom &amp;ldquo;lids&amp;rdquo; shut tightly and the top/bottom boards are at a &lt;code&gt;90°&lt;/code&gt; angle to the front.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="locators"&gt;Locators&lt;/h3&gt;&#10;&lt;p&gt;Now mount the locators aka bottom beam to the top and bottom of the cabinet. They will keep the side parts in place and the cabinet stiff.&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Mark the spots where locators will be placed at the side of the grove. You can find the dimensions &lt;a href="https://blog.schallbert.de/en/mobfobamp-diy/#plans"&gt;in this section&lt;/a&gt;.&lt;/li&gt;&#10;&lt;li&gt;Use a &lt;code&gt;5mm&lt;/code&gt; wood drill and put some tape on to mark &lt;code&gt;8mm&lt;/code&gt; depth so you wouldn&amp;rsquo;t completely go through the board.&lt;/li&gt;&#10;&lt;li&gt;Cut the 6 holes in each of top / bottom board.&lt;/li&gt;&#10;&lt;li&gt;Make sure the collar of the bottom beam is at level with the board surface, not with the grove.&lt;/li&gt;&#10;&lt;li&gt;Remove the locators for now, gaskets will have to be mounted before.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="gaskets"&gt;Gaskets&lt;/h3&gt;&#10;&lt;p&gt;They will finally make the cabinet airtight, but they&amp;rsquo;ll be under a large strain, especially if you fold and unfold the cabinet often. Some drops of silicon oil might increase their lifetime.&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Make sure the groves are clean and dry so the adhesive tape doesn&amp;rsquo;t fall off.&lt;/li&gt;&#10;&lt;li&gt;Start with 4 gaskets for each the top/bottom part. They might be a little wider than the grove, use your fingernails to fit them in.&lt;/li&gt;&#10;&lt;li&gt;When finished, take a razor or a knife with a pointed blade to cut holes in the gaskets where the locators will be pushed in. Use the marks for the locator holes you made earlier as guides.&lt;/li&gt;&#10;&lt;li&gt;Use a hammer to gently push the locators through the gaskets into place&lt;/li&gt;&#10;&lt;li&gt;Finally, use a small screwdriver or a blade to raise the gasket like a short sleeve around the locators - the cabinet must be sealed airtight.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/gaskets_mounted.jpg" alt="gaskets_topbottom_mounted"&gt;&lt;/figure&gt;&lt;/li&gt;&#10;&lt;li&gt;Apply the gaskets on the two sides.&lt;/li&gt;&#10;&lt;li&gt;Apply two gaskets per front / back board.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="auto-unfold"&gt;Auto-unfold&lt;/h3&gt;&#10;&lt;p&gt;This is an optional feature. With this installed, the cabinet will always want to unfold by itself and you will have less hassle getting ready to play.&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Take the four sides. With a Forstner bit, cut a &lt;code&gt;3mm&lt;/code&gt; deep circle right in the center of the insides of the side panels.&lt;/li&gt;&#10;&lt;li&gt;Cut a &lt;code&gt;70mm&lt;/code&gt; piece from the rubber tube. Cut it in half so you yield two straps.&lt;/li&gt;&#10;&lt;li&gt;Mount the strap on two of the sides using the &lt;code&gt;30mm&lt;/code&gt; spacer and a screw right within the circle you cut before.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/autounfold.jpg" alt="auto-unfold mounted to cabinet inner sides"&gt;&lt;/figure&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;The spacers are needed to increase the area where the rubber strap is secured against the wood. Without them, the rubber would tear around the screw.&lt;/p&gt;&#10;&lt;h3 id="hinges"&gt;Hinges&lt;/h3&gt;&#10;&lt;p&gt;Mounting the hinges with the gaskets on will be difficult. You might use another pair of hands or some clamps to help you out.&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Put the front panel on the table. Take the two sides with the gaskets on and have them aligned in a way that they &amp;ldquo;stand&amp;rdquo; on the gasket of the front board.&lt;/li&gt;&#10;&lt;li&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/scetch_pianohinge_corner.png" alt="inner corner hinge mounting"&gt;&lt;/figure&gt;Wile pressing the sides against the front board&amp;rsquo;s gaskets (orange), put the hinges (red) in place and mark their hole&amp;rsquo;s positions to simplify mounting.&lt;/li&gt;&#10;&lt;li&gt;Mount the hinges. Make sure there is no gap between side and front panel.&lt;/li&gt;&#10;&lt;li&gt;Repeat with the other two sides and the rear panel.&lt;/li&gt;&#10;&lt;li&gt;Put the two halves together. They will want to move inwards due to the gasket&amp;rsquo;s pressure, so use the strengthening panel to keep the cabinet fully unfolded. I was using a piece of wood with dimensions of &lt;code&gt;236 x 27 x 12&lt;/code&gt; for this step.&lt;/li&gt;&#10;&lt;li&gt;If you prepared the Auto-unfold feature, now is the time to pull the loose ends of the rubber strap and mount them on the opposite side.&lt;/li&gt;&#10;&lt;li&gt;Finally, mount the two hinges on either side of the cabinet&amp;rsquo;s outer surface. Make sure that there is no gap between the side parts.&lt;/li&gt;&#10;&lt;li&gt;Try the mechanism. If you fold the cabinet, it should unfold by itself in case you have the straps mounted correctly.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="top-and-bottom"&gt;Top and Bottom&lt;/h3&gt;&#10;&lt;p&gt;In this step, the locator holes are cut and the rubber hinge is mounted. This way, only two latches are needed instead of eight and the mechanical pressure on the gaskets is kept high enough.&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Keep the cabinet unfolded.&lt;/li&gt;&#10;&lt;li&gt;Apply some paint on the locators that are mounted in the top/ bottom parts already&lt;/li&gt;&#10;&lt;li&gt;mark the spots to drill locator holes on top and bottom of the cabinet by aligning and then pressing unfolded cabinet shell and top/ bottom parts together&lt;/li&gt;&#10;&lt;li&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/scetch_rubberhinge_rear.png" alt="rubber_hinge"&gt;&lt;/figure&gt;Use the &lt;code&gt;5mm&lt;/code&gt; drill to cut the holes both in top and bottom of the cabinet according to the marked dots, depth again &lt;code&gt;8mm&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Check if it all fits fine. Sit on it and try the latches. If the locators don&amp;rsquo;t fit 100%, take the drill and cheat a bit&amp;hellip;&lt;/li&gt;&#10;&lt;li&gt;Mount the top and bottom panels using the two remaining aluminium bars (green) and the loose end of the rubber hinge (black) to the back side of the cabinet like shown in the scetch.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="working"&gt;Working?!&lt;/h3&gt;&#10;&lt;p&gt;Whow, it&amp;rsquo;s almost done! Folding and unfolding should work by now, and you maybe should hear the sound of this cabinet before doing the finishing work&amp;hellip; &lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/cabinet_complete.jpg" alt="mounted cabinet"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Mount the speaker, but don&amp;rsquo;t forget to put in the gasket first. &lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/speaker_assembly.jpg" alt="speaker gaskets"&gt;&lt;/figure&gt; Three machine screws, M4 x15 with nuts will mount the speaker securely.&lt;/li&gt;&#10;&lt;li&gt;Configure the amplifier by soldering some jumpers. Follow the &lt;a href="https://learn.adafruit.com/adafruit-20w-stereo-audio-amplifier-class-d-max9744/analog-control" target="_blank" rel="noopener noreferrer" class="external-link"&gt;instructions from Adafruit&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; for the analog setup. Mount the potentiometer or set the amp to 100% amplification right away following the &lt;a href="https://blog.schallbert.de/en/mobfobamp-diy/#plans"&gt;circuit diagram&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Fuse and switch might not be needed yet. Instead of a battery, connect a power supply with current regulation, turn it to &lt;code&gt;12V @ 1A&lt;/code&gt; to protect from short circuits.&lt;/li&gt;&#10;&lt;li&gt;Quick, solder a &lt;code&gt;6.3mm&lt;/code&gt; input jack to a couple of wires or an old instrument cable of yours and shove it into the left channel&amp;rsquo;s terminals of your amp.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/electronics_assembly.jpg" alt="connect the electronics"&gt;&lt;/figure&gt;&lt;/li&gt;&#10;&lt;li&gt;Connect your instrument.&lt;/li&gt;&#10;&lt;li&gt;Turn the volume of your instrument up and enjoy!&lt;/li&gt;&#10;&lt;li&gt;Note: If your instrument is passive, i.e. has no built-in preamp, you&amp;rsquo;ll most likely need an additional preamp or an active DI box to get acceptable volume output. &lt;a href="https://www.tech21nyc.com/products/sansamp-2/bassdriver-di/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;E.g. this thing&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; will do the job very well, although most effects pedals also have a preamp onboard - I just used one of the latter.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="finishing-the-project"&gt;Finishing the project&lt;/h3&gt;&#10;&lt;p&gt;Look how sleek, lightweight and easy to carry your amp is when folded!&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/folded_standing.jpg" alt="folded MobFobAmp, standing"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;p&gt;Some additional steps I took were to give the amp an adequate housing (the empty screw case&amp;hellip;), add fuse, input jack(s), switch, LED and the battery. If you&amp;rsquo;re using an electric tool&amp;rsquo;s battery, normal cable lugs can connect the battery&amp;rsquo;s terminals to your setup. Using some spare wood and your router, it is an easy job to create a mount you can just click your battery in like I did for my design.&lt;/p&gt;&#10;&lt;p&gt;A handle might be a good idea for carrying, and a rubber strap could keep the cabinet in a folded position and the top/bottom parts attached to the rear when folded.&lt;/p&gt;&#10;&lt;p&gt;I sincerely hope you enjoyed the read / the build and that you&amp;rsquo;ll have as much fun taking this little thing anywhere to play as I do.&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;Schallbert&lt;/em&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://blog.schallbert.de/en/projects/mobfobamp/"&gt;Back to Project main page&lt;/a&gt;&lt;/p&gt;&#10;</description></item><item><title>Markdown to PDF</title><link>https://blog.schallbert.de/en/md-export-pdf/</link><pubDate>Wed, 09 Jun 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/md-export-pdf/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/markdown-thumb.jpg"&#10; class="post-cover"&#10; alt="markdown to pdv converter extension for VS code"&#10; title="Markdown to PDF" /&gt;&#10;&lt;h3 id="the-tool"&gt;The tool&lt;/h3&gt;&#10;&lt;p&gt;Get a tool that can do the job. I selected the Visual Studio Code extension &lt;a href="https://github.com/yzane/vscode-markdown-pdf" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Yzane&amp;rsquo;s Markdown PDF&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Easy to install, easy to use, good documentation. Only the images&amp;hellip; well&amp;hellip; wouldn&amp;rsquo;t show.&lt;/p&gt;&#10;&lt;h3 id="getting-the-images-converted-too"&gt;Getting the images converted, too&lt;/h3&gt;&#10;&lt;p&gt;The relative links to the images are different between Jekyll&amp;rsquo;s interpretation and the notion from the PDF generator. I had to move the target markdown file into the root folder and add a &lt;code&gt;.&lt;/code&gt; before my &lt;code&gt;/assets&lt;/code&gt; to make it work. Pagination is &amp;hellip;meh&amp;hellip; but I&amp;rsquo;m not willing to put more work in this right now so that&amp;rsquo;s as good as it gets.&lt;/p&gt;&#10;&lt;h3 id="an-issue-with-posts-not-showing-up"&gt;An issue with Posts not showing up?&lt;/h3&gt;&#10;&lt;p&gt;Just upon publishing this sixth post of mine, I see that somehow only page 2 of the post overview is shown (looks like 5 posts per page) and I cannot switch to page 1 containing &amp;ldquo;latest&amp;rdquo; posts: &lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-06-09_stuckonpage2.jpg" alt="terminal output"&gt;&lt;/figure&gt;. Hm.&lt;/p&gt;&#10;</description></item><item><title>Jekyll: images</title><link>https://blog.schallbert.de/en/jekyll-images/</link><pubDate>Mon, 07 Jun 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/jekyll-images/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/git_mv-thumb.jpg"&#10; class="post-cover"&#10; alt="github move operation for renaming"&#10; title="Jekyll: images" /&gt;&#10;&lt;h3 id="the-trick-to-make-images-show-both-locally-and-on-github-pages"&gt;The trick to make images show both locally and on Github pages&lt;/h3&gt;&#10;&lt;p&gt;When I added some images to my content, all was fine until I pushed to GitHub. Most images were gone and I couldn&amp;rsquo;t figure out why. Some &lt;a href="https://stackoverflow.com/questions/41468951/images-not-displaying-in-github-pages#41469181" target="_blank" rel="noopener noreferrer" class="external-link"&gt;searching&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; revealed that Github is case sensitive and some of my image paths were still mixed case.&lt;/p&gt;&#10;&lt;h3 id="changing-folder-names-in-git"&gt;Changing folder names in git&lt;/h3&gt;&#10;&lt;p&gt;Well, it still didn&amp;rsquo;t work because git didn&amp;rsquo;t recognize my renaming. I first had to create the renamed folder in another subdirectory, use &lt;code&gt;git mv folder/tArGet* target&lt;/code&gt; to get this to a temporary place, delete &lt;code&gt;tArGet&lt;/code&gt; and move &lt;code&gt;target&lt;/code&gt; to the place where &lt;code&gt;tArGet&lt;/code&gt; was before. Only this fixed the issue and now github and my locals are in sync again, luckily.&lt;/p&gt;&#10;</description></item><item><title>This site: Table of Contents</title><link>https://blog.schallbert.de/en/jekyll-toc/</link><pubDate>Sun, 06 Jun 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/jekyll-toc/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/thissite/desktop-thumb.jpg"&#10; class="post-cover"&#10; alt="Schallbert&amp;#39;s Desktop"&#10; title="This site: Table of Contents" /&gt;&#10;&lt;h2 id="table-of-contents"&gt;Table of Contents&lt;/h2&gt;&#10;&lt;p&gt;There is an easy solution for making the page&amp;rsquo;s table of contents stick:&#10;add &lt;code&gt;toc_sticky: true&lt;/code&gt; to the defaults or the specific page&amp;rsquo;s front matter. How I found the keyword? By searching &amp;ldquo;toc&amp;rdquo; in the whole solution and some scrolling through the layout scetches. I modified the original site navigation in the left sidebar and exchanged it with a dummy implementation of a gallery / audio / video contents display.&lt;/p&gt;&#10;&lt;h3 id="toc-on-the-left"&gt;ToC on the left&lt;/h3&gt;&#10;&lt;p&gt;To understand how to create a sticky ToC on the left, please continue reading &lt;a href="https://blog.schallbert.de/en/projects/thissite/#sidebar"&gt;this chapter&lt;/a&gt; of the mySite project.&lt;/p&gt;&#10;&lt;h2 id="authors-profile"&gt;Author&amp;rsquo;s profile&lt;/h2&gt;&#10;&lt;p&gt;I decided I only want my &lt;code&gt;author_profile&lt;/code&gt; to be shown on pages like &lt;a href="https://blog.schallbert.de/en/about/"&gt;about&lt;/a&gt;, &lt;a href="https://blog.schallbert.de/en/legal/"&gt;legal info&lt;/a&gt; and not on the typical content pages. So I wrote &lt;code&gt;author_profile: false&lt;/code&gt; for the pages I didn&amp;rsquo;t want it to show up - with no effect. Why?&#10;I had set it in the &lt;code&gt;config.yml&lt;/code&gt;&amp;rsquo;s type defaults but it should actually go into the frontmatter of the individual files.&lt;/p&gt;&#10;</description></item><item><title>Jekyll `-incremental` option</title><link>https://blog.schallbert.de/en/jekyll-items-update/</link><pubDate>Thu, 03 Jun 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/jekyll-items-update/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/jekyll-thumb.jpg"&#10; class="post-cover"&#10; alt="Jekyll logo"&#10; title="Jekyll `-incremental` option" /&gt;&#10;&lt;h3 id="recent-posts-again"&gt;Recent Posts again&lt;/h3&gt;&#10;&lt;p&gt;I figured out the reason why my last &amp;ldquo;open item&amp;rdquo; post didn&amp;rsquo;t show up on the landing page on my local machine but worked fine on remote. It is because, locally, I&amp;rsquo;m using the command &lt;code&gt;$ bundle exec jekyll serve --incremental&lt;/code&gt; with the &lt;code&gt;--incremental&lt;/code&gt; build option active to speed up rebuilds for quicker test runs. But the issue is here: &lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-06-03_jekyll_incremental.jpg" alt="terminal output"&gt;&lt;/figure&gt;&#10;On an incremental output, &lt;em&gt;jekyll&lt;/em&gt; doesn&amp;rsquo;t seem to scan the &lt;code&gt;_posts&lt;/code&gt; folder for new entries, as the page they appear on has not been modified directly.&#10;My takeaway is that I&amp;rsquo;ll be using the &lt;code&gt;--incremental&lt;/code&gt; option with care and for small/quick changes only in future.&lt;/p&gt;&#10;&lt;h3 id="the-images"&gt;The images&lt;/h3&gt;&#10;&lt;p&gt;I worked on my &lt;code&gt;assets/images&lt;/code&gt;, they tend to be somewhat &amp;ldquo;big&amp;rdquo; so that page load times might be a reason to worry about. I&amp;rsquo;m using &lt;a href="https://www.getpaint.net/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;paint.net&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; to shrink them to a size I think I can afford and save them as &lt;code&gt;.jpg&lt;/code&gt; with compression max&amp;rsquo;ed out and some compromises on image quality. This way, most of my pictures take less than 10% or the original size in kB.&lt;/p&gt;&#10;</description></item><item><title>MinimalMistakes theme - Page design</title><link>https://blog.schallbert.de/en/jekyll-open-items/</link><pubDate>Wed, 02 Jun 2021</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/jekyll-open-items/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/mmistakes-thumb.jpg"&#10; class="post-cover"&#10; alt="MinimalMistakes logo"&#10; title="MinimalMistakes theme - Page design" /&gt;&#10;&lt;h2 id="the-landing-page"&gt;The landing page&lt;/h2&gt;&#10;&lt;p&gt;Yeah, by changing the layout type to &lt;code&gt;single &lt;/code&gt;I was able to get a header image as type &lt;code&gt;overlay&lt;/code&gt; in!&#10;The buttons are displayed within the image now. The image changes with display size and orientation automatically due to the visual designs defined in the &lt;code&gt;_sass&lt;/code&gt; folder.&lt;/p&gt;&#10;&lt;h3 id="landing-page-image"&gt;landing page image&lt;/h3&gt;&#10;&lt;p&gt;Ha, found it! I was freaking out; my landing page now just wouldn&amp;rsquo;t show any image, no matter how hard I tried modifying the &lt;code&gt;home.md&lt;/code&gt; file&amp;hellip; Reason behind was that there was a stock &lt;code&gt;index.html&lt;/code&gt; file that came with the template and that seems to have priority. No I just added my changes in there - works perfectly fine!&#10;By the way, the overlay image&amp;rsquo;s height is automatically selected depending on the overlay text&amp;rsquo;s contents/ height.&lt;/p&gt;&#10;&lt;h3 id="adding-a-page-navigation"&gt;Adding a page navigation&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Easy solution: type &lt;code&gt;toc: true&lt;/code&gt; in the front matter. Unfortunately, eventually you will loose this information as you scroll down because it won&amp;rsquo;t stick to your scrolled view.&lt;/li&gt;&#10;&lt;li&gt;Hard solution: Add page navigation to left sidebar. How? &lt;a href="https://blog.schallbert.de/en/projects/thissite/#navigation"&gt;See here!&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="recent-posts"&gt;Recent posts&lt;/h3&gt;&#10;&lt;p&gt;Hell, what am I writing this down for? It does not seem to appear in the &amp;ldquo;Recent Posts&amp;rdquo; column of the landing page. Oh, that&amp;rsquo;s just because I would have to restart the service because the posts are generated statically &lt;a href="https://blog.schallbert.de/en/jekyll-items-update/"&gt;Right, Jekyll is a &lt;strong&gt;static&lt;/strong&gt; site generator&amp;hellip;&lt;/a&gt;&lt;/p&gt;&#10;</description></item><item><title>🎄 Breadboard Fireplace</title><link>https://blog.schallbert.de/en/projects/fireplace/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/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="LED-Fireplace, Arduino-controlled"&#10; title="🎄 Breadboard Fireplace" /&gt;&#10;&lt;h2 id="project-stats"&gt;Project stats&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Difficulty: beginner 1/5&lt;/li&gt;&#10;&lt;li&gt;Cost: 8&amp;hellip;25€&lt;/li&gt;&#10;&lt;li&gt;Time: ~1h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="abstract"&gt;Abstract&lt;/h2&gt;&#10;&lt;p&gt;This is a quick and easy project that demonstrates how to use an embedded device and some LEDs to create a warm and comfortable living room environment. Tied to a powerbank, this little thing will glow all evening and create a warm and comfy atmosphere. I borrowed parts of the idea for this project from a book called &amp;ldquo;Tiny AVR for the evil Genius&amp;rdquo; which I read when I started with embedded software design in &lt;code&gt;C&lt;/code&gt; years ago.&lt;/p&gt;&#10;&lt;p&gt;Project cost varies on the components you choose. If you take an original Arduino, it will be on the upper end - but in this case you pay tribute to the hard-working developers and engineers at &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;, creating open source solutions for all of us 🥳&lt;/p&gt;&#10;&lt;h2 id="the-hardware"&gt;The Hardware&lt;/h2&gt;&#10;&lt;p&gt;Of course, you can use any microcontroller board that supports at least 8 digital outputs and 1 analog input. In my example I&amp;rsquo;m using an &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; replica board I had flying around somewhere. Time to upgrade to the &amp;ldquo;original&amp;rdquo;&amp;hellip;&#10;You should have a small breadboard, a couple of wires and eight LEDs at the ready. I recommend using 5 warm-white LEDs, 2 yellow LEDs, and 1 red LED with similar brightness (lumen count) to yield colors that are similar to a real fire.&lt;/p&gt;&#10;&lt;h2 id="the-software"&gt;The Software&lt;/h2&gt;&#10;&lt;p&gt;The software creates a flickering lights effect with varying update speed.&lt;/p&gt;&#10;&lt;h3 id="setup"&gt;Setup&lt;/h3&gt;&#10;&lt;p&gt;On startup, in &lt;code&gt;setup()&lt;/code&gt; I define pins the LEDs are connected to as outputs.&#10;I make it read an open analog pin once and takes its (undefined) value as seed for the pseudo random number generator discussed later.&lt;/p&gt;&#10;&lt;h3 id="loop"&gt;Loop&lt;/h3&gt;&#10;&lt;p&gt;In the infinite loop, the program calculates a new random number and then hands this number over to the downstream functions. Here, a random number generator creates value which is then used to&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;determine which of the eight LEDs should be on&lt;/li&gt;&#10;&lt;li&gt;set the time for which this LED status shall be maintained.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Finally, the LEDs are set accordingly and the loop repeats at the beginning.&lt;/p&gt;&#10;&lt;h3 id="linear-feedback-shift-register"&gt;Linear Feedback Shift Register&lt;/h3&gt;&#10;&lt;p&gt;The Random Number Generator is implemented as a so-called &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; with a length of 32bit (repeats its pattern after max. 2^32 steps). This implementation uses the &amp;ldquo;Galois type&amp;rdquo; of LFSRs.&lt;/p&gt;&#10;&lt;h3 id="galois-lfsr-implementation"&gt;Galois LFSR implementation&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;There&amp;rsquo;s a lot going on in this line of code, so please let me explain it down to the core.&lt;/p&gt;&#10;&lt;p&gt;Essential to this sort of random number generators is the &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; indicated by a &lt;code&gt;^&lt;/code&gt; in the center of the code line. &lt;code&gt;XOR&lt;/code&gt; means &amp;ldquo;eXclusive OR&amp;rdquo;, which outputs logic &lt;code&gt;1&lt;/code&gt; only when logic inputs &lt;code&gt;A&lt;/code&gt; and &lt;code&gt;B&lt;/code&gt; are &lt;em&gt;different&lt;/em&gt;. When &lt;code&gt;A&lt;/code&gt; and &lt;code&gt;B&lt;/code&gt; are both &lt;code&gt;0&lt;/code&gt;, or both &lt;code&gt;1&lt;/code&gt;, this gate will output &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Example binary XOR:&lt;/strong&gt;&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt; A B Out&#10;0b00001111 ^ 0b00110011 = 00111100&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Left to the &lt;code&gt;XOR&lt;/code&gt; operator, the current randum number is shifted to the right (&lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;) by &lt;code&gt;1&lt;/code&gt;. All of its contents is made less significant by one bit. In numerical terms it means that a value is integer-divided by two.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Example binary SHIFT:&lt;/strong&gt;&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt; A B Out numerical:&#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, let&amp;rsquo;s further analyze the above code line.&#10;Right to the &lt;code&gt;XOR&lt;/code&gt; operator, there&amp;rsquo;s another operation taking place: A logic &lt;code&gt;AND&lt;/code&gt; (&lt;code&gt;&amp;amp;&lt;/code&gt;) between our current random number and &lt;code&gt;1u&lt;/code&gt; which means &amp;ldquo;unsigned 1&amp;rdquo;. What it does is masking all higher bits of the randum number generator, the result of this action is a simple &lt;code&gt;0&lt;/code&gt; in case the number has a &lt;code&gt;0&lt;/code&gt; as its least significant bit and vice versa.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Example binary AND:&lt;/strong&gt;&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt; A B Out &#10;0b00001111 &amp;amp; 1 = 0b00000001&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The negation at the beginning of this term means that, when the result of the before &lt;code&gt;&amp;amp;&lt;/code&gt; was &lt;code&gt;0&lt;/code&gt;, the whole term will be &lt;code&gt;0b00000000...&lt;/code&gt; again. If it&amp;rsquo;s &lt;code&gt;-1&lt;/code&gt;, the 2&amp;rsquo;s complement binary code will instead look like this: &lt;code&gt;0b1111111...&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;This is an easy way to apply a single boolean value to any longer data type without branching logic like using an &lt;code&gt;if()&lt;/code&gt; clause.&lt;/p&gt;&#10;&lt;p&gt;And finally, there&amp;rsquo;s another logic &lt;code&gt;AND&lt;/code&gt; between this term and a fixed bitmask &lt;code&gt;0xd0000001u&lt;/code&gt;. The latter are the &amp;ldquo;taps&amp;rdquo; of the LFSR, i.e. which bits of the current value are fed back into the system. Unravelling this hex value to boolean shows: &lt;code&gt;0xd = 0b1101&lt;/code&gt; so the bits 32, 31, 29, and 1 are &lt;code&gt;&amp;amp;&lt;/code&gt;&amp;lsquo;ed with the current random number, leaving only their values untouched while all others are set to &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="galois-lfsr-summary"&gt;Galois LFSR Summary&lt;/h3&gt;&#10;&lt;p&gt;Whew, that was a lot. It&amp;rsquo;s time for a couple of minutes break. Then please read the summary below for the beloved code line&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;Meaning: &amp;ldquo;If the least significant bit of the current random number is zero, then just shift the random number to the right. Else, in addition XOR with the tapped bits of the current random number.&amp;rdquo;&lt;/em&gt;&lt;/p&gt;&#10;&lt;p&gt;LFSRs are widely used in &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;wireless communication technology&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, they are interesting for &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;checksum calculations&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://medium.com/@czapfel/an-introduction-to-lfsrs-for-cryptography-bf2602640e91" target="_blank" rel="noopener noreferrer" class="external-link"&gt;cryptographers&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and can even be useful when &lt;a href="https://evoniuk.github.io/posts/pitfall.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;programming games&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, e.g. for procedural map generation. LFSRs are easy to implement (as you can see they even may only use a single line fo code!) and don&amp;rsquo;t use many resources. Here&amp;rsquo;s an excellent &lt;a href="https://datagenetics.com/blog/november12017/index.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;blog post&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; dealing with LFSRs in depth if you&amp;rsquo;re more the visual type (and less the logic one).&#10;On the down side, their &amp;ldquo;quality of randomness&amp;rdquo; is bad because they are deterministic.&#10;All in all I found it a very interesting topic which is why I decided to manually implement an LFSR here instead of using Arduino&amp;rsquo;s stock &lt;code&gt;random()&lt;/code&gt; function.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://www.c64-wiki.de/images/9/9d/Pitfall_Animation2.gif" alt="Pitfall! Game scene"&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Pitfall! Game scene: procedural level design with LFSR&lt;/span&gt;&lt;a&#10; href="https://www.c64-wiki.de/wiki/Datei:Pitfall_Animation2.gif#file"&#10; class="attr-link"&#10; aria-label="Attribution 1"&#10; &gt;&#10; &lt;sup class="attr-id"&gt;[1]&lt;/sup&gt;&#10; &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h3 id="possible-issue"&gt;Possible issue&lt;/h3&gt;&#10;&lt;p&gt;Well, if you fully understood the code line, you see what happens if the random number contains only &lt;code&gt;0&lt;/code&gt;s.&lt;/p&gt;&#10;&lt;p&gt;It will never get out of this state again 💀&lt;/p&gt;&#10;&lt;p&gt;That&amp;rsquo;s why you should never initialize this type of LFSR with &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="driving-the-leds"&gt;Driving the LEDs&lt;/h3&gt;&#10;&lt;p&gt;The rest of the software has much more lines, but also does much more boring stuff. It takes the generated 32-bit random value and runs it past the 8 LED outputs, i.e. every LED &amp;ldquo;sees&amp;rdquo; each bit of the value. Between each of these iterations, a randomized delay is used so the flickering speed is limited to a change rate we still can perceive.&lt;/p&gt;&#10;&lt;h2 id="the-hardware-1"&gt;The Hardware&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/fireplace/fireplace_detail.jpg" alt="fireplace breadboard design detail view"&gt;&lt;/figure&gt;&#10;Thankfully, the hardware is more easy to explain than the software:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;An Arduino Nano board is connected to a prototyping board.&lt;/li&gt;&#10;&lt;li&gt;Eight LED&amp;rsquo;s anodes &lt;code&gt;(+)&lt;/code&gt; are connected to the digital outputs &lt;code&gt;D2...D9&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Their cathodes are bridged with jumpers and connected to the Arduino&amp;rsquo;s &lt;code&gt;GND&lt;/code&gt;.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;This is the simplest design I could think of to do the job.&lt;/p&gt;&#10;&lt;h2 id="improvements"&gt;Improvements&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Each LED should get a series resistor.&#10;&lt;ul&gt;&#10;&lt;li&gt;In my design, the current is only limited by the maximum current the Arduino Pin is able to drive.&lt;/li&gt;&#10;&lt;li&gt;Either the LED can handle well above &lt;code&gt;40mA&lt;/code&gt; peak current or it will inevitably break down at some point in time.&lt;/li&gt;&#10;&lt;li&gt;Resistor calculation should happen with the LED datasheet in mind as different LED colours have different voltages.&lt;/li&gt;&#10;&lt;li&gt;Example resistor calculation &lt;code&gt;@5V&lt;/code&gt; supply: Warm-White LED: &lt;code&gt;20mA @ 3.1V --&amp;gt; R = U/I = (Usup-Uled) / I = 1.9 / 0.02 = 95Ohm&lt;/code&gt;. Take &lt;code&gt;100Ohm&lt;/code&gt;.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;Each LED could get a parallel capacitor.&#10;&lt;ul&gt;&#10;&lt;li&gt;This would smooth the flickering effect.&lt;/li&gt;&#10;&lt;li&gt;I could take videos in which the camera wasn&amp;rsquo;t irritated by LED flickering. That would be nice.&lt;/li&gt;&#10;&lt;li&gt;Example Capacitor calculation: Average current when switched &amp;ldquo;off&amp;rdquo; &lt;code&gt;20mA&lt;/code&gt;, allowed voltage drop &lt;code&gt;0.6V&lt;/code&gt; within &lt;code&gt;1ms&lt;/code&gt;. Proposed Capacitance is then &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="you-want-a-diy-replica"&gt;You want a DIY replica?&lt;/h2&gt;&#10;&lt;p&gt;Here you go!&#10;&lt;a href="https://blog.schallbert.de/en/led-fireplace-diy/"&gt;Breadboard Fireplace DIY instructions&lt;/a&gt;&lt;/p&gt;&#10;</description></item><item><title>1 year CNC 🎂</title><link>https://blog.schallbert.de/en/projects/one-year-zerspanobert/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/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 year CNC 🎂" /&gt;&#10;&lt;h2 id="one-year-with--portal-milling-cnc-machine"&gt;One year with &lt;a href="https://blog.schallbert.de/en/portal-milling-build/"&gt;&amp;lsquo;Zerspanobert&amp;rsquo;&lt;/a&gt; portal milling CNC machine&lt;/h2&gt;&#10;&lt;p&gt;It&amp;rsquo;s March 2023, and I&amp;rsquo;m hitting a year of operation with my CNC machine. It was a good year. I definitely learned a lot and had quite some fun writing articles about my progress. Of course, I wasn&amp;rsquo;t able to document and blog about each and every project that I ran during that period, so take this article as a retrospect summary of the year.&lt;/p&gt;&#10;&lt;h3 id="the-machine"&gt;The machine&lt;/h3&gt;&#10;&lt;p&gt;With 12 months of experience, I can say that I selected the right machine. It is Sorotec&amp;rsquo;s second-cheapest one, featuring a lot of stuff that more advanced portal milling machines have like ball screws, &lt;code&gt;3Nm&lt;/code&gt; steppers, &lt;code&gt;48V&lt;/code&gt; low voltage rail, preloaded linear guides. To keep the price tag moderate it misses higher-end features like belt drives, dual linear guides on the Z-axis, and additional stiffeners for portal, z-axis, and main frame.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/z_axis.jpg" alt="Image: Z-axis detail view"&gt;&lt;/figure&gt;&#10;&lt;p&gt;That&amp;rsquo;s why you have to know which loads are borderline and pull back on those. But that&amp;rsquo;s likely true also for more advanced machines, they only raise the limits. What blocks quicker feed rates or higher depths of cut within my setting currently is the router motor and the way it is attached to the Z-axis.&lt;/p&gt;&#10;&lt;p&gt;The motor is clearly overwhelmed with&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;birch multiplex, full-slot milling, 6mm cutter 2-flute, &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, full-slot milling, 6mm cutter 2-flute &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, full-slot milling, 6mm cutter single flute, &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;If you reduce depth of cut and/or feed rate however, it will be able to run the job very well.&#10;Machine accuracy is pretty good; the actually limiting factors here are the manual determination of XY0 visually and the non-calibratable tool length sensor, not the machine itself.&lt;/p&gt;&#10;&lt;p&gt;The vacuum table opened up the world of fine engravings with a V-cut. Problem was that before, workpiece warp and inaccuracies of the workbed caused engravings to be of very mixed quality and larger tolerances in width than I had anticipated. These issues are now gone, as well as the additional work of creating fixtures for small or cumbersome Workpieces. Holding and clamping got easier by a magnitude, and I could reduce Z-safety height which would speed up engravings by as much as 20%.&lt;/p&gt;&#10;&lt;p&gt;I focused on reducing machining time as much as possible so I wouldn&amp;rsquo;t have to spend more time than necessary monitoring the machine in the cold workshop. That&amp;rsquo;s why I deliberately brought it and the cutters to their limits in terms of rapid movement speed, feed rate, and depth of cut. When I figured out these limits, I dialed back on the parameters by 20-30% to stay safe and keep repeatability high.&lt;/p&gt;&#10;&lt;h3 id="upgrades"&gt;Upgrades&lt;/h3&gt;&#10;&lt;p&gt;I upgraded my CNC with a &lt;a href="https://blog.schallbert.de/en/why-vacuum-table/"&gt;vacuum table&lt;/a&gt; and had to learn a lot about clamping forces and &lt;a href="https://blog.schallbert.de/en/cnc-vacuum-pumps/"&gt;vacuum pump selection&lt;/a&gt;. I installed acoustics insulation to get noise level and low-frequency vibrations down. It&amp;rsquo;s hard to see in the below image, but the whole machine table&amp;rsquo;s underside has been covered in sound- and vibration absorber mats.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&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;p&gt;Also, a chip extraction system was added so I wouldn&amp;rsquo;t have all the dust in my workshop. It works flawlessly. The amount of chips produced is astonishing and I&amp;rsquo;m glad to have it installed since the early days.&lt;/p&gt;&#10;&lt;p&gt;I will soon add a dedicated 3-phase spindle motor to further minimize vibrations and tolerances induced into the motor mount. The machine will become a little more quiet, yet more powerful so that I may finally be able to reach the target feed rates mentioned above.&lt;/p&gt;&#10;&lt;h3 id="materials"&gt;Materials&lt;/h3&gt;&#10;&lt;p&gt;I have been working with hard wood, &lt;a href="https://blog.schallbert.de/en/negative-carving-with-estlcam/#engrave"&gt;multiplex&lt;/a&gt;, &lt;a href="https://blog.schallbert.de/en/projects/cnc_spoilboard/"&gt;MDF&lt;/a&gt;, acrylic (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/en/cnc-router-overload/"&gt;High pressure laminates&lt;/a&gt;, and &lt;a href="https://blog.schallbert.de/en/cut-dibond/"&gt;Aluminium composites&lt;/a&gt;.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&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;p&gt;With all of these, I had to learn how the materials behave and with which parameters to run the endmills for high quality results. Some materials were so challenging that my machine was overloaded, with some others I had &lt;a href="https://blog.schallbert.de/en/cnc-vibrates/"&gt;vibration issues&lt;/a&gt; or problems to &lt;a href="https://blog.schallbert.de/en/cut-dibond-tests/"&gt;raise the bar on quality&lt;/a&gt; but I always learned from these failures and improved my processes so they could be circumvented.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/pmma.jpg" alt="Image: Acrylic Chip exctraction hose holder"&gt;&lt;/figure&gt;&#10;&lt;p&gt;The image above shows an extraction hose clamp that I mounted to the Z-axis of my CNC, made of PMMA (Acrylic). I worked hard to get minimal chamfers just right and to utilize smoothing runs where I thought they could provide better surface finishes.&lt;/p&gt;&#10;&lt;h3 id="software"&gt;Software&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/qr-codengrave/app-thumb.jpg" alt="Image: Qr-codengrave main app screen"&gt;&lt;/figure&gt;&#10;I tweaked the machine&amp;rsquo;s &lt;a href="https://blog.schallbert.de/en/macros-for-cnc/"&gt;macros&lt;/a&gt;, &lt;a href="https://blog.schallbert.de/en/portal-milling-setup/"&gt;tuned its kinematics parameters&lt;/a&gt;, and even wrote a full-blown application to enable the CNC to engrave &lt;a href="https://blog.schallbert.de/en/projects/qr-codengrave/"&gt;QR-codes&lt;/a&gt; into virtually any sheet material, providing a high rate of detection for pretty much any camera.&lt;/p&gt;&#10;&lt;p&gt;When my engraving projects got more and more complex, my CAM kept crashing when calculating paths for batch processing. Thus I had to do batches manually which made me dive into coordinate shift commands like &lt;code&gt;G54...G59.3&lt;/code&gt;, &lt;code&gt;G68&lt;/code&gt;, and &lt;code&gt;G92&lt;/code&gt; which will kill your workpieces if not properly understood.&#10;This helped me better understand the numerical controller and its interpreter language.&lt;/p&gt;&#10;&lt;p&gt;Last but not least the learning curve I had to get used to working with &lt;a href="https://blog.schallbert.de/en/freecad-get-started/"&gt;FreeCAD&lt;/a&gt; and &lt;a href="https://blog.schallbert.de/en/negative-carving-with-estlcam/"&gt;Estlcam&lt;/a&gt; had to be pretty steep, or else I wouldn&amp;rsquo;t have been able to get my parts cut.&lt;/p&gt;&#10;&lt;h3 id="help--support"&gt;Help &amp;amp; support&lt;/h3&gt;&#10;&lt;p&gt;Luckily, I always had people who were both willing and able to help. My wife was always there to support and allowed me to make time for this, and my kids were visiting me in the workshop frequently so that I didn&amp;rsquo;t feel lonely at any time. It is my wife and some great friends to whom I could talk to when work got frustrating. But I also had outstanding support from the commercial side:&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--left"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/sorotec.jpg" alt="Image: Sorotec logo"&gt;&lt;/figure&gt;&#10;Thanks to Willy and Roy at &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; for advice, tipps and tricks that helped me handle my machine, and to get better lifetime out of my endmills. I always enjoyed your direct phone support and quick turnaround times for cutter delivery and guarantee repairs. I also like to visit the &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; for discussions and to publish and discuss my stories there to give something back to the community.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--left"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/eding.jpg" alt="Image: EdingCNC logo"&gt;&lt;/figure&gt;&#10;Also thanks to the &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; team, especially to Iwona and Pim for your timely and in-depth responses on support tickets and controller software related questions.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--left"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/stritzelberger.jpg" alt="Image: Stritzelberger logo"&gt;&lt;/figure&gt;&#10;Many thanks go to Jo from &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; for hours and hours of discussion about good solutions for my vacuum table and its accessories.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--left"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/vhf.jpg" alt="Image: vhg logo"&gt;&lt;/figure&gt;&#10;From &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;, I&amp;rsquo;d like to thank Mrs. Dressner for the time and frequent conversations about quality of cut, optimizations of feeds and speeds for the company&amp;rsquo;s endmills, and advice on material milling strategies.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--left"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/benezan.jpg" alt="Image: Benezan logo"&gt;&lt;/figure&gt;&#10;Also thanks to Nicolas from &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; for assisting me with analysis of a problem with and, finally, replacement of the signal breakout board.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--left"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/gmc.jpg" alt="Image: Gossen Metrawatt logo"&gt;&lt;/figure&gt;&#10;Thanks to Mr. Leibold from &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; for helping me select a good instrument for measuring voltage peaks and system power draw.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--left"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/spinogy.jpg" alt="Image: Spinogy logo"&gt;&lt;/figure&gt;&#10;Cheers to Dominik from &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; with whom I had phone calls throughout well over half a year to select a proper spindle upgrade and its accessories for my machine.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--left"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/cadascam.jpg" alt="Image: CadAsCam logo"&gt;&lt;/figure&gt;&#10;Finally, thanks to Achim from &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;, your friendly ear taking software improvement proposals, answers to basic questions, and your extremely quick response with software updates and fixes.&lt;/p&gt;&#10;&lt;h2 id="runtime-and-jobs"&gt;Runtime and jobs&lt;/h2&gt;&#10;&lt;p&gt;Within the year, I ran 200 jobs with a total machine time of less than 18 hours. When counting in workpiece setup, machine calibration and so on, I&amp;rsquo;d triple that number. And that result again can be doubled easily when taking time for CAM/CAD into account, too.&lt;/p&gt;&#10;&lt;p&gt;In terms of time, I can&amp;rsquo;t tell exactly how many hours I devoted to the machine itself. I believe it is on par with maintaining this website, where the creation of 1 minute of &amp;ldquo;time to read&amp;rdquo; is about one hour that I need to invest.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/jobs_1year.jpg" alt="Image: CNC software readout"&gt;&lt;/figure&gt;&#10;&lt;p&gt;I also spent hours and hours online researching for material parmeters, cutters, taking and giving advice in forums, investigating machine upgrades, and watching videos about how other people use their CNCs.&lt;/p&gt;&#10;&lt;p&gt;By the way, my current favorite (as of March-2023) is by 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;). I won&amp;rsquo;t try to compare myself to that but I&amp;rsquo;m always impressed by people and manufacturers who know their stuff.&lt;/p&gt;&#10;&lt;h2 id="cost"&gt;Cost&lt;/h2&gt;&#10;&lt;p&gt;The CNC hobby is &lt;em&gt;expensive&lt;/em&gt;, both in terms of money and time. In my case, I invested nearly 13.000,00€ 😨 into machine, its accessories, spindle, machine table, power supply and protective equipment, lighting, numerical controller, laptop, CAD/CAM/CNC software, acoustic insulation, tools to build the machine, exhaust extraction system, personal safety equipment etc.&lt;/p&gt;&#10;&lt;p&gt;And with that, I didn&amp;rsquo;t even mention cost for endmills.&lt;/p&gt;&#10;&lt;p&gt;This is how it is: &amp;ldquo;The machine itself is not too expensive&amp;rdquo;, you think. But once you decide to buy, you enter a rabbit hole. I followed it down almost all its paths, making very few compromises on quality of equipment, and not specialising in one material which would have kept costs down.&lt;/p&gt;&#10;&lt;p&gt;I was intrigued by the different materials and their properties, and it was always a joy when a part turned out really good, independent off the matter it consisted of.&lt;/p&gt;&#10;&lt;h2 id="failures--solutions"&gt;Failures &amp;amp; solutions&lt;/h2&gt;&#10;&lt;p&gt;Here&amp;rsquo;s a list of bigger issues I experienced within the last year.&lt;/p&gt;&#10;&lt;h3 id="router-motor-stop-intermittent-operation-"&gt;Router motor stop, intermittent operation. Ⓜ️&lt;/h3&gt;&#10;&lt;p&gt;This issue was reproducible in hard wood and multiplex after just 8h of spindle operation. Solution was the installation of a new electronics PCB that Maffell supplied without further ado.&lt;/p&gt;&#10;&lt;h3 id="mid-job-router-motor-speed-drop-to-minimum-rpm-"&gt;Mid-job router motor speed drop to minimum RPM Ⓜ️&lt;/h3&gt;&#10;&lt;p&gt;This issue was reproducible as well and had two key causes:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/emergency.jpg" alt="Image: Emergency switch detail view"&gt;&lt;/figure&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Emergency switch had very little travel for triggering, so there were short contact bounces possible. The switch is mounted to the machine table, and could be triggered through machine vibrations. I fixed this by modifying the microswitch&amp;rsquo;s position in the housing so that the switch wouldn&amp;rsquo;t trigger early.&lt;/li&gt;&#10;&lt;li&gt;The breakout PCB that generates the analogue &lt;code&gt;0-10V&lt;/code&gt; signal out of the CNC&amp;rsquo;s pulse width command had a wrong operational amplifier type mounted (non-RTR). Under yet unknown circumstances (maybe contact bounce of emergency switch?), this part would experience latch-down so the router motor speed would drop to its lowest possible setting.&#10;A new signal breakout board was mounted and the issue disappeared.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="velocityerror"&gt;CNC: &amp;ldquo;Velocity was higher than max!&amp;rdquo; ⚠️&lt;/h3&gt;&#10;&lt;p&gt;This error appeared on a few occasions. They were all unpleasant. Once, I pushed my machine too hard (&lt;code&gt;240mm/s&lt;/code&gt; on XY, &lt;code&gt;110mm/s&lt;/code&gt; on Z), which made it squeak and loose a lot of steps, adding up into the centimeter range.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/stepfrequency_old.jpg" alt="Image: overclocked machine kinematics that lead to issues"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Another time with less high (but still too high, retrospectively) velocity values entered, I made a mistake by commanding a rapid move &lt;code&gt;G00 Xxx Yyy Zzz&lt;/code&gt; for all axes simultaneously which made the machine jump and crash into its mechanical limits on Z-axis. Thankfully, it was the upper limit so neither the spindle nor the machine or its bed suffered any damage. Only the stepper clutch slipped. Whew.&lt;/p&gt;&#10;&lt;p&gt;The reason: Maximum &lt;em&gt;combined axes move&lt;/em&gt; pulse frequency is &lt;code&gt;125kHz&lt;/code&gt;. I considered it was per-axis. This is &lt;em&gt;wrong&lt;/em&gt;. I double-checked it with the numerical controller manufacturer. I needed to readjust the axis&amp;rsquo;s speed values to stay below &lt;code&gt;125kHz&lt;/code&gt; &lt;em&gt;combined&lt;/em&gt;.&lt;/p&gt;&#10;&lt;p&gt;To find new values, I looked at my jobs so far. I was loosing more time on Z-axis rapid movements than on the other two axes. That&amp;rsquo;s why I put emphasis on the Z-axis to keep it quick and with high acceleration while I reduced the values for X- and Y-axis. Note that Z-axis has double resolution due to its shorter ball screw threading, so you have to double its values to compare them with the other axes.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/stepfrequency_new.jpg" alt="Image: tuned machine kinematics fixing the issue"&gt;&lt;/figure&gt;&#10;&lt;p&gt;This all happened after improving manufacturing quality that I &lt;a href="https://blog.schallbert.de/en/cut-dibond-tests/#machine-vibrations"&gt;documented here&lt;/a&gt;, so you can consider these values are proven to yield good results, thus won&amp;rsquo;t be changed again lightly.&lt;/p&gt;&#10;&lt;h3 id="running-the-machine-into-the-vacuum-table-"&gt;Running the machine into the vacuum table 💥&lt;/h3&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/vacuumtable_cut.jpg" alt="Image: 6mm cutter marks in my brand-new vacuum table"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Well, that really upset me. I didn&amp;rsquo;t even have the table for two weeks, and already now I had damaged it. Worst thing was that I didn&amp;rsquo;t understand why.&lt;/p&gt;&#10;&lt;p&gt;All I wanted was to cut 18mm birch multiplex. I had the rougher do the outline but faced an issue that made me press the emergency switch when the cut already was complete (Travelling back to XY0, the cutter would have hit an obstacle). I fixed the path and reset the machine. I changed tools to do the smoothing, and hit &amp;ldquo;run&amp;rdquo;.&lt;/p&gt;&#10;&lt;p&gt;When I realized that the machine sounded unusual, I hit the emergency stop. But at over &lt;code&gt;4000mm/min&lt;/code&gt; for smoothing I wasn&amp;rsquo;t quick enough to avoid the machine run through the vacuum table.&lt;/p&gt;&#10;&lt;p&gt;I looked at the &lt;code&gt;G92&lt;/code&gt; work offset for Z-axis. It was correct. I searched in the CNC&amp;rsquo;s volatile variables. All matched my expectations. Then I started digging into the machine&amp;rsquo;s logfiles. Luckily, I made the above picture right after this happened so it wasn&amp;rsquo;t hard to figure out the correct log lines.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/edinglog.jpg" alt="Image: log file showing lool length measurements"&gt;&lt;/figure&gt;&#10;&lt;p&gt;You can see here that tool length has been measured both for the rougher and the standard cutter. The standard cutter protrudes more - by roughly &lt;code&gt;3.4mm&lt;/code&gt;difference. This is the exact depth that I was missing on my vacuum table now.&lt;/p&gt;&#10;&lt;p&gt;Still, I didn&amp;rsquo;t have a clue what caused the machine to not take the updated tool length into account. So I looked at the Tool Change macro.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/macro_rootcause.jpg" alt="Image: macro showing lool length measurement routine"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Here you can see that variable &lt;code&gt;#3501&lt;/code&gt; captures whether a tool length has been taken already. This is vital because if so, you can just measure the new tool length and take the difference between the two, save that as a Z-axis offset and you&amp;rsquo;re fine to proceed the program with the new tool.&lt;/p&gt;&#10;&lt;p&gt;The thing is that variable &lt;code&gt;#3501&lt;/code&gt; is volatile as revealed during a good read of the controller&amp;rsquo;s manual. And it seems like volatile variables are not only reset when the application is closed, but also when the emergency switch is pressed.&lt;/p&gt;&#10;&lt;p&gt;Deducing from my prior statements, I had the following issue: The emergency switch press would wipe that variable&amp;rsquo;s value but keep the original Z0 height measurement that I did at the very beginning before using the rougher bit. So, instead of updating the height offset with the new tool to account for the new tool length, the macro was thinking that this was the first tool used &lt;em&gt;and just saved&lt;/em&gt; its length to variable &lt;code&gt;#4501&lt;/code&gt; (because you can&amp;rsquo;t take any difference if you didn&amp;rsquo;t have a tool prior to the first one, consequencially).&lt;/p&gt;&#10;&lt;p&gt;This alone wouldn&amp;rsquo;t even have caused this incident if the newly entered cutter hadn&amp;rsquo;t protruded more from the spindle collet than the one before.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/1year_cnc/macro_solution.jpg" alt="Image: Interim solution was to modify the macro"&gt;&lt;/figure&gt;&#10;&lt;p&gt;I solved this problem by adding a message into the macro when it was solely saving the tool length but not modifying the Z-axis&amp;rsquo;s offset. This way I&amp;rsquo;d at least have an indication when something is about to go wrong.&lt;/p&gt;&#10;&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;&#10;&lt;p&gt;In summary, I will continue using the CNC. I will be learning as I go. And I will deepen my knowledge about how software interacts with mechatronics that changes physical matter like &amp;lsquo;Zerspanobert&amp;rsquo; does. I&amp;rsquo;ll try to write down what I learned so others can follow my tracks if they like to.&lt;/p&gt;&#10;&lt;p&gt;I hope to be able to maintain good contact to other hobbyists and makers, and to the companies that I had the pleasure to work with so far (and, maybe, to add some more to my list).&lt;/p&gt;&#10;&lt;p&gt;Cheers!&#10;&lt;em&gt;Schallbert&lt;/em&gt;&lt;/p&gt;&#10;</description></item><item><title>About</title><link>https://blog.schallbert.de/en/about/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/about/</guid><description type="html">&#10;&lt;h2 id="purpose-of-this-website"&gt;Purpose of this Website&lt;/h2&gt;&#10;&lt;p&gt;I&amp;rsquo;m running &lt;a href="https://blog.schallbert.de/en/projects/thissite/"&gt;this website&lt;/a&gt; on a voluntary basis. I do this because I&amp;rsquo;d like to spark others&amp;rsquo; interest and fun in the areas of Mechanics, Electronics, and Information Technology. I focus particularly on the manufacture of items using CNC milling technologies, as well as on the setup, operation, deployment and maintenance of my server and its content. Occasional articles on electronics development and IT security round off the blog.&lt;/p&gt;&#10;&lt;h3 id="about-ai"&gt;About &amp;lsquo;AI&amp;rsquo;&lt;/h3&gt;&#10;&lt;p&gt;This website is and will remain free of content generated by &amp;lsquo;AI&amp;rsquo; tools. The articles are written by hand (I think you can tell) and I have no intention of changing this. For translations, however, I do use &amp;lsquo;AI&amp;rsquo;-based translation tools. In the early days up to Q1 2023, this applied exclusively to German articles, as I had been running the blog in English.&lt;/p&gt;&#10;&lt;p&gt;Since then, however, I have been writing in German and having the articles translated into English, because &amp;lsquo;AI&amp;rsquo; finds this much easier and, in my opinion, produces better results. Nevertheless, I proofread every translated post and make every effort to publish high-quality and interesting articles.&lt;/p&gt;&#10;&lt;p&gt;The research and everything else involved on this blog is always done manually.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;Consequently, for every minute of reading time, I have to invest roughly an hour&amp;rsquo;s work.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Where I do use “AI”, however, is as a “rubber ducky” for ideas and solutions that I wouldn&amp;rsquo;t have come up with myself due to a lack of perspective. Essentially, I use it as an advanced search engine. But even this output doesn&amp;rsquo;t feature in the articles.&lt;/p&gt;&#10;&lt;h2 id="who-is-schallbert"&gt;Who is Schallbert?&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--left"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/root/Schallbert.avif" alt="Image: A computer-drawn image of Schallbert, pixelized, and in 256 colors."&gt;&lt;/figure&gt;&#10;I&amp;rsquo;m Schallbert.&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;&#10;Hobbyist maker and programmer, Electronics technician, Electrical Engineer. I have been (and am still) working in the industry for 10+ years both as hardware systems/ research engineer, project lead, software developer and scrum master so far. I have a passion for playing the Bass.&lt;/p&gt;&#10;&lt;h3 id="interesting-name-how-did-you-get-it"&gt;Interesting name. How did you get it?&lt;/h3&gt;&#10;&lt;p&gt;Schallbert is a portmanteau of the words &lt;code&gt;Schall&lt;/code&gt; (a &lt;a href="https://de.wikipedia.org/wiki/Schall" target="_blank" rel="noopener noreferrer" class="external-link"&gt;sound propagating through space&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;) and &lt;code&gt;Bert&lt;/code&gt; (a &lt;a href="https://de.wikipedia.org/wiki/Bert_%28Name%29" target="_blank" rel="noopener noreferrer" class="external-link"&gt;first name&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;). Phonetically similar to &lt;em&gt;Albert&lt;/em&gt;, I originally chose it as a player name: it is short and catchy, rare enough to work as a nickname without having to add numbers or glyphs, and, if you like, you can read into it a love of music and admiration for Albert Einstein.&lt;/p&gt;&#10;&lt;h3 id="contact"&gt;Contact&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;via Github Discussions: &lt;a href="https://github.com/Schallbert/schallbert.github.io/discussions/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;schallbert.github.io&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;through my channel at PeerTube: &lt;a href="https://makertube.net/c/cnc_corner/videos?s=1" target="_blank" rel="noopener noreferrer" class="external-link"&gt;CNC corner at MakerTube&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;via PlatformIO (µC-topics): Your account &amp;ndash;&amp;gt; messages &amp;ndash;&amp;gt; compose &lt;a href="https://community.platformio.org/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PIO Community&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;via Sorotec Forum (CNC-Topics): &lt;a href="https://hobbyline.info/forum/index.php?thread/603-blog-artikel-zur-portalfr%C3%A4se/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;hobbyline.info&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;via CNCzone Forum (CNC-Themen): &lt;a href="https://www.cnczone.nl/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;cnczone.nl&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; -&amp;gt; Private Message&lt;/li&gt;&#10;&lt;li&gt;Directly (not preferred): See &lt;a href="https://blog.schallbert.de/en/legal/"&gt;legal&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&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;Image courtesy of &lt;a href="https://github.com/Likosdev" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Likosdev&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>AnywhereAmps - Alpha</title><link>https://blog.schallbert.de/en/projects/aa_alpha/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/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="AnywhereAmps Alpha&amp;#39;s insides"&#10; title="AnywhereAmps - Alpha" /&gt;&#10;&lt;p&gt;Mobile Instrument Amplifier/Cabinet combo prototype based on &lt;a href="https://blog.schallbert.de/en/shelly13/"&gt;Shelly13&lt;/a&gt;. AnywhereAmps Alpha has a low power, small footprint aproach. It is designed as a very portable full frequency range practice amplifier with a total weight of only &lt;code&gt;3.1kg&lt;/code&gt;. It features adjustable gain and a &lt;code&gt;9V DC&lt;/code&gt; supply for external effects pedals.&lt;/p&gt;&#10;&lt;h2 id="project-stats"&gt;Project stats&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Difficulty: medium 3/5&lt;/li&gt;&#10;&lt;li&gt;Cost: ~170€&lt;/li&gt;&#10;&lt;li&gt;Time: 10h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="impressions"&gt;Impressions&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&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-0"&#10; aria-label="Bild: AnywhereAmps Alpha, aufrecht stehend."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/aa_alpha/upright.jpg"&#10; alt="Bild: AnywhereAmps Alpha, aufrecht stehend."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-1"&#10; aria-label="Bild: AnywhereAmps Alpha, mit Spannungsversorgung für Bodeneffektpedale"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/aa_alpha/effects_on.jpg"&#10; alt="Bild: AnywhereAmps Alpha, mit Spannungsversorgung für Bodeneffektpedale"&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-2"&#10; aria-label="Bild: AnywhereAmps Alpha eingeschaltet, Fokus auf die Bedienelemente."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/aa_alpha/switched_on.jpg"&#10; alt="Bild: AnywhereAmps Alpha eingeschaltet, Fokus auf die Bedienelemente."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-3"&#10; aria-label="Image: AnywhereAmps Alpha, top view"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/aa_alpha/top.jpg"&#10; alt="Image: AnywhereAmps Alpha, top view"&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-4"&#10; aria-label="Image: AnywhereAmps Alpha, side view"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/aa_alpha/side.jpg"&#10; alt="Image: AnywhereAmps Alpha, side view"&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-5"&#10; aria-label="Image: AnywhereAmps Alpha Amplifier Boards"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/aa_alpha/amp.jpg"&#10; alt="Image: AnywhereAmps Alpha Amplifier Boards"&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;/div&gt;&lt;div class="hugo-gallery__caption"&gt;&#10; Impressions of AnywhereAmps Alpha's prototyping&#10; &lt;/div&gt;&lt;/div&gt;&#10;&#10; &lt;div class="hugo-gallery__full" aria-live="polite"&gt;&lt;figure&#10; id="gallery-full-0"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/aa_alpha/upright.jpg"&#10; alt="Bild: AnywhereAmps Alpha, aufrecht stehend."&gt;&lt;figcaption&gt;Can be played upright, especially suited for very small practice rooms.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-1"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/aa_alpha/effects_on.jpg"&#10; alt="Bild: AnywhereAmps Alpha, mit Spannungsversorgung für Bodeneffektpedale"&gt;&lt;figcaption&gt;9V voltage converter for floor pedals and accessories.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-2"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/aa_alpha/switched_on.jpg"&#10; alt="Bild: AnywhereAmps Alpha eingeschaltet, Fokus auf die Bedienelemente."&gt;&lt;figcaption&gt;Minimalistic HMI: On/off switch, gain, 1/4&amp;#39;&amp;#39; jack, 9V out&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-3"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/aa_alpha/top.jpg"&#10; alt="Image: AnywhereAmps Alpha, top view"&gt;&lt;figcaption&gt;12&amp;#39;&amp;#39; diameter, 2.6Gallons of volume&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-4"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/aa_alpha/side.jpg"&#10; alt="Image: AnywhereAmps Alpha, side view"&gt;&lt;figcaption&gt;Battery on the back plate. Pull up to remove.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-5"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/aa_alpha/amp.jpg"&#10; alt="Image: AnywhereAmps Alpha Amplifier Boards"&gt;&lt;figcaption&gt;Preamp and poweramp are stacked on top of each other.&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&#10;&lt;/section&gt;&#10;&#10;&lt;h2 id="amp-sound-samples"&gt;Amp sound samples&lt;/h2&gt;&#10;&lt;h3 id="bass-samples"&gt;Bass samples&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--center media-frame--audio"&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;Predominantly high notes&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--audio"&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;Played really hard&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--audio"&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;Open strings &amp;amp; flageolet notes&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;All tracks recorded with Zoom&amp;rsquo;s H4n using an &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; from 1992 with the neck pickups @100%, full volume, low string setting.&lt;/p&gt;&#10;&lt;h3 id="dimensions"&gt;Dimensions&lt;/h3&gt;&#10;&lt;p&gt;Raw drum shell: &lt;code&gt;12&amp;quot;x6&amp;quot;&lt;/code&gt;. Total height is around &lt;code&gt;17cm&lt;/code&gt;, width &lt;code&gt;30cm&lt;/code&gt;, depth (without battery) at &lt;code&gt;32cm&lt;/code&gt;. The enclosed volume incl. amp and speakers is &lt;code&gt;~9l&lt;/code&gt;. I&amp;rsquo;m using a non-vented cabinet design.&lt;/p&gt;&#10;&lt;h3 id="sound-hardware"&gt;Sound Hardware&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Speakers: &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; Bass/Mid&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and two &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 tweeters&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 amplifier&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; with &lt;code&gt;30W&lt;/code&gt; RMS power (bridged)&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;14.4V&lt;/code&gt; Li-Ion tool battery supply&lt;/li&gt;&#10;&lt;li&gt;All sensitive parts on the inside of the cabinet&lt;/li&gt;&#10;&lt;li&gt;Custom built preamp with &lt;code&gt;0...27dB&lt;/code&gt; adjustable gain&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="simulation"&gt;Simulation&lt;/h3&gt;&#10;&lt;p&gt;The simulation shows that the volume of ~9l damped cabinet would be a perfect match for the selected 4&amp;quot; speaker. It will go down as low as &lt;code&gt;44Hz&lt;/code&gt; (&lt;code&gt;F#1&lt;/code&gt;, one full note above e.g. a Bass&amp;rsquo;s lowest open string &lt;code&gt;E1&lt;/code&gt;). There will be punchy lows at a precision which is still good. The downside is the low efficiency of just &lt;code&gt;78dB/W/m&lt;/code&gt;, so the expected maximum volume I&amp;rsquo;ll get from the power input will be rather low and the speaker will be driven into its excursion limits early.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&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;h2 id="custom-preamp"&gt;Custom Preamp&lt;/h2&gt;&#10;&lt;p&gt;The class-D amplifier I selected requires a line-level input, so I had to add a preamp to my design to get sufficient volume from the power stage. Even the cheapest Preamps like &lt;a href="https://artproaudio.com/product/tube-mp-the-original/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;this one from Art Pro Audio&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; would add considerable cost to the design. They would weigh a lot and consume quite some power, especially if they are tube preamps, and often have special requirements on input voltage (like AC only). Plus, their design wouldn&amp;rsquo;t blend into mine.&lt;/p&gt;&#10;&lt;p&gt;So I decided to design a low power, minimal parts gain stage myself. &lt;a href="https://blog.schallbert.de/en/projects/aa_alpha/#sound-quality"&gt;Click here to skip the nerdy Tech details&lt;/a&gt;&lt;/p&gt;&#10;&lt;h3 id="voltage-rails"&gt;Voltage rails&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/virtualground.jpg" alt="Virtual Ground Circuit"&gt;&lt;/figure&gt;As the preamp would amplify alternating voltage signals but I only have one battery voltage rail available, first I had to think about creating a &lt;a href="https://en.wikipedia.org/wiki/Virtual_ground" target="_blank" rel="noopener noreferrer" class="external-link"&gt;virtual ground&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; potential that would be the point of signal zero for the gain stage. Luckily, there are a lot of &lt;a href="https://mil.ufl.edu/4924/docs/TI_SingleSupply_OpAmp.pdf" target="_blank" rel="noopener noreferrer" class="external-link"&gt;design guidelines&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; about how to operate the amplifier with a single voltage source, so I had something to start with. I used &lt;code&gt;10kOhm&lt;/code&gt; resistors to clamp the inputs of the gain stage to the virtual ground.&lt;/p&gt;&#10;&lt;h3 id="gain-stage"&gt;Gain stage&lt;/h3&gt;&#10;&lt;p&gt;AnywhereAmps Alpha should be a multi-purpose instrument combo, so that there should be a wide gain range available to bring everything from a passive guitar to a line level Keyboard to the same voltage levels for the power stage to deal with. That&amp;rsquo;s why I decided to go for a &lt;a href="http://www.sengpielaudio.com/calculator-levelchange.htm" target="_blank" rel="noopener noreferrer" class="external-link"&gt;0-27dB amplification range&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and selected resistors for the feedback loop to be &lt;code&gt;10kOhm&lt;/code&gt; and a &lt;code&gt;250kOhm&lt;/code&gt; logarithmic potentiometer.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/gainstage.jpg" alt="Gain stage circuit"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;h3 id="circuit-simulation"&gt;Circuit simulation&lt;/h3&gt;&#10;&lt;p&gt;I used Analog Device&amp;rsquo;s &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; to simulate how this circuit might behave when given a typical Audio signal between &lt;code&gt;20H&lt;/code&gt;z and &lt;code&gt;20kHz&lt;/code&gt;. From my studies I remembered that I might be most interested in a so-called Bode plot which would list both amplitude and phase over frequency in a double-logarithmic scale. Conveniently, LTspice offers such a &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;function&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&#10;As you can see, the preamp&amp;rsquo;s output - green - stays flat into the &lt;code&gt;10kHz&lt;/code&gt; range and the phase would only shift a little while the input line -blue - stays &lt;code&gt;~27dB&lt;/code&gt; below.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/bodeplot.jpg" alt="Gain stage circuit"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;h3 id="op-amp-selection"&gt;Op-Amp selection&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/crossover_distortion.jpg" alt="Crossover Distortion"&gt;&lt;/figure&gt;Well. I tried this design on my breadboard with multi-purpose Rail-to-Rail &lt;a href="https://en.wikipedia.org/wiki/Operational_amplifier" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Operational amplifier&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; I had lying around and it worked&amp;hellip; not well at all. When I connected a function generator at the input and measured what was supposed to be a sine wave at the output of the &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;, I didn&amp;rsquo;t get a sine wave at the output at all, but a half-sine with some steps and discharge-alike curves around &lt;code&gt;0&lt;/code&gt;, then followed by the negative half-sine&amp;hellip; After some search I found out this is called &amp;ldquo;&lt;a href="https://en.wikipedia.org/wiki/Crossover_distortion" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Crossover distortion&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&amp;rdquo; and is caused by the asymmetric output stage of this specific op amp type. I was able to correct this a bit when applying a higher load current on the output, but finally I wanted to choose an op amp which was better suited for my audio application without rail-to-rail capability and lower noise ratios. Thankfully, there are quite some pages on that topic like &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;. Weighing in a reasonable piece price and the availability at my electronics distributor, I selected the &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;. The video below shows what I got from the updated op-amp selection.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&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;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Preamping a Sine wave&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h3 id="circuit-design"&gt;Circuit design&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/circuit_board.jpg" alt="Crossover Distortion"&gt;&lt;/figure&gt; I wanted my circuit board to use as little space as possible while keeping DC voltage supply panes like a frame on the outsides of the board. The board&amp;rsquo;s left half is dedicated to creating the virtual ground potential while the right half does the amplification part. The design is minimal, using only as much as four resistors and three capacitors.&lt;/p&gt;&#10;&lt;p&gt;For a technical deep dive, refer to my &lt;a href="https://blog.schallbert.de/en/preamp-design-considerations/"&gt;design considerations&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="sound-quality"&gt;Sound quality&lt;/h2&gt;&#10;&lt;p&gt;AnywhereAmps Alpha is absolutely quiet in idle, even with gain full up there&amp;rsquo;s only so much as a little hissing from the tweeters. No hum, no scratching when turning the gain knob. It works with any instrument due to the wide gain range using &lt;code&gt;0dB&lt;/code&gt; for line levels like Keyboards, preamped instruments, mobile phone, and up to &lt;code&gt;+27dB&lt;/code&gt; for passive Bass or Guitar direct in. Maximum volume is fine although the low-lows &amp;ldquo;flatten down&amp;rdquo; too early, limiting usable volume range e.g. for organs and 5-string basses. For frequencies in the Bass range at high volume, it still comes down to Physics - the bigger the better. Find some sound samples &lt;a href="https://blog.schallbert.de/en/projects/aa_alpha/#amp-sound-samples"&gt;up here.&lt;/a&gt;&lt;/p&gt;&#10;&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha/shell_raw.jpg" alt="Raw drum shell"&gt;&lt;/figure&gt;&#10;&lt;p&gt;For the cabinet, I ordered some raw custom size Tom-tom shells from &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;. Cutting and especially hand-routing a curved surface proved to be hard, and the fact that I don&amp;rsquo;t own a proper wood workshop and did all this work on the balcony of my flat didn&amp;rsquo;t make it easier.&#10;But after roughly &lt;code&gt;10h&lt;/code&gt; incl. soldering and electronics work, I now have more than a super-portable instrument combo.&lt;/p&gt;&#10;&lt;p&gt;It&amp;rsquo;s a piece of furniture.&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;m going to be playing it a lot. Plus, the good sound keeps me more motivated than ever to go the next step and boost reproduction of the low frequency range with another, larger prototype.&lt;/p&gt;&#10;&lt;h2 id="pros"&gt;Pros:&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;I like the design.&lt;/li&gt;&#10;&lt;li&gt;Fits into standard Drum bags!&lt;/li&gt;&#10;&lt;li&gt;Lightweight, &lt;code&gt;3.1kg&lt;/code&gt; incl. Battery, &lt;code&gt;2.6kg&lt;/code&gt; without&lt;/li&gt;&#10;&lt;li&gt;Battery state of charge can be read at a glance&lt;/li&gt;&#10;&lt;li&gt;Retracted controls: Avoids inadvertent operation&lt;/li&gt;&#10;&lt;li&gt;More robust: All electronics and speaker chassis mounted inside&lt;/li&gt;&#10;&lt;li&gt;includes &lt;code&gt;9V, 1.6A&lt;/code&gt; power supply jack for effects, stomp pedals, etc.&lt;/li&gt;&#10;&lt;li&gt;Sounds good for any instrument as it plays almost the full audible frequency spectrum&lt;/li&gt;&#10;&lt;li&gt;Runs &lt;code&gt;&amp;gt;20h&lt;/code&gt; on a battery charge (&lt;code&gt;14.4V, 4Ah&lt;/code&gt;) as a practising amplifier and moderate volume&lt;/li&gt;&#10;&lt;li&gt;Runs &lt;code&gt;&amp;gt;3h&lt;/code&gt; on a battery charge at maximum volume&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="imperfections"&gt;Imperfections:&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Too small for sub-bass reproduction.&lt;/li&gt;&#10;&lt;li&gt;No auto mute on switching the amp on or off -&amp;gt; Pop Noise&lt;/li&gt;&#10;&lt;li&gt;Of course, not enough volume to match a drumset or plugged band setup.&lt;/li&gt;&#10;&lt;li&gt;Looks weird with big battery packs on the back. Use of a &amp;ldquo;flat pack&amp;rdquo; would be nice&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="anywhereamps-alpha-diy-replica"&gt;AnywhereAmps Alpha: DIY Replica&lt;/h2&gt;&#10;&lt;p&gt;You want do build your own copy? Sure, use the step-by-step guide below and get going!&#10;&lt;a href="https://blog.schallbert.de/en/anywhereamps-diy/"&gt;AnywhereAmps Alpha DIY instructions&lt;/a&gt;&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&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;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Alpha live!&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;</description></item><item><title>AnywhereAmps - Alpha 1.1</title><link>https://blog.schallbert.de/en/projects/aa_alpha_1_1/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/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;Mobile Instrument Amplifier/Cabinet combo prototype Version 1.1, evolving from &lt;a href="https://blog.schallbert.de/en/projects/aa_alpha/"&gt;AnywhereAmps Alpha&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="project-stats"&gt;Project stats&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Difficulty: medium 3/5&lt;/li&gt;&#10;&lt;li&gt;Cost: ~200€ plus battery system&lt;/li&gt;&#10;&lt;li&gt;Time: 3h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="updates-over-alpha-10"&gt;Updates over Alpha 1.0:&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Add &lt;a href="https://blog.schallbert.de/en/configure-max1756x/"&gt;power supervisor circuitry&lt;/a&gt; to protect both amplifier and battery pack from abnormal voltage levels / currents&lt;/li&gt;&#10;&lt;li&gt;Integrate on/off switch in potentiometer&lt;/li&gt;&#10;&lt;li&gt;Reduce complexity by using high efficiency full-range speaker instead of 3-speaker 2-way system&lt;/li&gt;&#10;&lt;li&gt;Add high efficiency switching buck converter to generate 9V effects voltage rail&lt;/li&gt;&#10;&lt;li&gt;Save construction time by using &lt;a href="https://blog.schallbert.de/en/anywhereamps-get-pcb/"&gt;custom printed circuit board&lt;/a&gt; featuring preamp, step-down converter, and power supervisor&lt;/li&gt;&#10;&lt;li&gt;Save construction time by delegating most of the woodworks to a &lt;a href="https://blog.schallbert.de/en/portal-milling/"&gt;portal milling machine&lt;/a&gt; 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;&lt;/li&gt;&#10;&lt;li&gt;Eliminate switch-on pop noise by adding a RC-filter to the standby input of the power amp stage&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="build-variants"&gt;Build variants&lt;/h3&gt;&#10;&lt;p&gt;To get progress on sound for my prototyping, I built different variants this time. I changed speakers and evaluated the sound:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Alpha 1.0: 30W bridged amp, 4&amp;quot; bass and 2x piezo tweeters: HiFi-esque sound with punchy lows and clear highs, limited on the max volume side&lt;/li&gt;&#10;&lt;li&gt;Alpha 1.1 v1: 30W bridged amp, 4&amp;quot; PA driver, good volume and high presence, but underrepresented lows and highs&lt;/li&gt;&#10;&lt;li&gt;Alpha 1.1 v2: 30W bridged amp, 4&amp;quot; PA driver and passive membrane, fixing low-end&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="my-own-preamp-and-auxiliary-functions-printed-circuit-board"&gt;My own preamp and auxiliary functions printed circuit board&lt;/h2&gt;&#10;&lt;p&gt;The &lt;a href="https://blog.schallbert.de/en/pcbs-arrived/"&gt;AnywhereAmps 1.1 PCB&lt;/a&gt; features some extra functionality apart from the mere preamp plus lowpass and highpass for bi-amping. The details can be found in the following sections.&lt;/p&gt;&#10;&lt;h3 id="power-supervisor"&gt;Power Supervisor&lt;/h3&gt;&#10;&lt;p&gt;I added the power supervisor circuitry to not allow the battery to run completely empty. I also wanted to mitigate risks created by short circuits and other fault conditions which could harm the battery. The integration of this technology worked on first try (which I hadn&amp;rsquo;t expected). The only drawback is that when the battery gets discharged to below, say 10%, the amp would sometimes restart on power bursts as supply voltage would drop below the supervisor&amp;rsquo;s trigger voltage. As the battery voltage would go up again when load reduces, it will turn back on a second later.&lt;/p&gt;&#10;&lt;p&gt;This is clearly &lt;a href="https://blog.schallbert.de/en/capacitor-problems/"&gt;undesired behavior&lt;/a&gt; and will have to be handled with a refined prototype level.&#10;Another thing that makes this follow-up prototype necessary is the fact that I am not allowed to use the tool manufacturer&amp;rsquo;s batteries in my future designs &lt;a href="https://blog.schallbert.de/en/no-can-use-metabo/"&gt;as discussed here&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="9v-switching-buck-converter"&gt;9V switching buck converter&lt;/h3&gt;&#10;&lt;p&gt;To generate auxiliary 9V for devices like effects, loopers etc. I wanted to reduce noise by selecting a &amp;ldquo;quiet&amp;rdquo; switching regulator. The one I chose was designed for automotive applications and has the inductors included in its enclosure. The down sides are that the footprint is not suitable for hand soldering and the high price tag.&lt;/p&gt;&#10;&lt;p&gt;The converter would switch on when a device is plugged into the corresponding jack so that there&amp;rsquo;s no stand-by current. It operates quitely (but not completely without noise) and more than enough power for the applications I encountered so far - A looper, compressor, and tuner can be easily operated in parallel.&lt;/p&gt;&#10;&lt;h2 id="woodworking"&gt;Woodworking&lt;/h2&gt;&#10;&lt;p&gt;I bought a portal milling machine set to delegate the woodworking for AnywhereAmps to. I wanted to spend less time on cutting and sanding, and improve build quality as well as reapeatability. But changing the manual woodworks to more or less automated machining, I had to first move my plans into the digital world.&lt;/p&gt;&#10;&lt;h3 id="computer-aided-design"&gt;Computer Aided Design&lt;/h3&gt;&#10;&lt;p&gt;I chose FreeCAD as design tool to create drawings. I documented the work with this tool in a couple of &lt;a href="https://blog.schallbert.de/en/freecad-get-started/"&gt;posts&lt;/a&gt; and continued to refine my workflow e.g. &lt;a href="https://blog.schallbert.de/en/projects/seifenbutler/#v1-v3-first-drafts-in-hpl"&gt;here&lt;/a&gt;. My first scetch to manufacture looked like this.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha11/alpha11_cad.jpg" alt="Image: FreeCAD drawing snapshot of AnywhereAmps Alpha 1.1"&gt;&lt;/figure&gt;&#10;&lt;p&gt;I was not completely happy with partitioning the sheet of wood yet. I found the legs too long and put some more hours in refining the design to use less space as the handling capacity of my portal milling machine is limited.&lt;/p&gt;&#10;&lt;h3 id="computer-aided-manufacturing"&gt;Computer Aided Manufacturing&lt;/h3&gt;&#10;&lt;p&gt;When done, I exported the scetches as &lt;code&gt;*.dxf&lt;/code&gt; files (Drawing Interchange File Format (DXF), introduced by AutoDesk in 1982. It eventually became industry standard for interchanging 2D drawings) and opened them in my CAM tool to define tool paths, set XY zero, and Z depth per pass.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha11/alpha11_cam.jpg" alt="Image: CAM snapshot of AnywhereAmps Alpha 1.1"&gt;&lt;/figure&gt;&#10;&lt;h3 id="g-code"&gt;G-Code&lt;/h3&gt;&#10;&lt;figure class="media-frame media-frame--center"&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;p&gt;I ended up with G-Code files post-processed to match my machine with a forecast duration of 32 minutes per sheet. I was using just three tools: a 6mm endmill for the cutouts, a 2mm one for smaller holes and structures, and an engraving tool for chamfers and logo.&#10;I partitioned the work into three jobs:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha11/finishing.jpg" alt="Image: job02_finishing: carving the logo"&gt;&lt;/figure&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;code&gt;job00_clamping&lt;/code&gt; to cut holes and countersinks into the sheet to allow attachment to the workbed&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;job01_nesting&lt;/code&gt; to do all cutouts and engravings on the ampcarrier, but leave taps so the parts don&amp;rsquo;t move&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;job02_finishing&lt;/code&gt; which cuts the logo on the (already coloured) top side and adds chamfers where needed&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;For finishing, I had to update CAD and CAM files so they would provide a mirrored (and turned by -90°) view of the sheet. This is to allow two-sided milling with the same XY-zero point and the exact same sides touching the fence for minimal tolerances.&lt;/p&gt;&#10;&lt;h3 id="manual-work-remaining"&gt;Manual work remaining&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha11/waxit.jpg" alt="Image: waxing the wooden surfaces"&gt;&lt;/figure&gt;&#10;I had to do a little manual work like sanding down taps, adding wax to get the top finish done, and cutting some holes for struts and stands that my CNC couldn&amp;rsquo;t do because they are on a plane orthogonal to the Z-axis.&lt;/p&gt;&#10;&lt;p&gt;I used the CNC to create adapters for my hand router so I could have it glide over the bent surface of the shell. This way, the holes that I cut in there for the speakers before could be properly chamfered.&lt;/p&gt;&#10;&lt;p&gt;I also had the CNC manufacture a shell holder in 3D-mode (with &lt;code&gt;*.STL&lt;/code&gt; files). This way, the shell is kept in place when cutting holes for the speakers with my hand drill. By the way: 3D cuts take a lot of time as finishing uses small stepovers to get a smooth surface. I&amp;rsquo;d like to further improve shell handling; hand-cutting a &amp;gt;100mm hole does not allow good surface quality on the inside of the shell, and handling that huge drill is still a pain. Unfortunately, my CNC is not able to to this job for me.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/aa_alpha11/shell_adapter.jpg" alt="Image: Shell holder"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;h2 id="impressions"&gt;Impressions&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&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-0"&#10; aria-label="Image: AnywhereAmps Alpha V1.1, switched on, with battery attached"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/aa_alpha11/battery.jpg"&#10; alt="Image: AnywhereAmps Alpha V1.1, switched on, with battery attached"&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-1"&#10; aria-label="Image: AnywhereAmps Alpha V1.1, front view"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/aa_alpha11/front_top.jpg"&#10; alt="Image: AnywhereAmps Alpha V1.1, front view"&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-2"&#10; aria-label="Image: AnywhereAmps Alpha V1.1 bottom view"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/aa_alpha11/bottom.jpg"&#10; alt="Image: AnywhereAmps Alpha V1.1 bottom view"&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-3"&#10; aria-label="Image: CNC-cut parts alongside with speaker and shell"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/aa_alpha11/parts.jpg"&#10; alt="Image: CNC-cut parts alongside with speaker and shell"&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-4"&#10; aria-label="Image: AnywhereAmps Alpha V1.1, a view inside"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/aa_alpha11/top_open.jpg"&#10; alt="Image: AnywhereAmps Alpha V1.1, a view inside"&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-5"&#10; aria-label="Image: Alpha V1.1&amp;#39;s body side view"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/aa_alpha11/side.jpg"&#10; alt="Image: Alpha V1.1&amp;#39;s body side view"&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;/div&gt;&lt;div class="hugo-gallery__caption"&gt;&#10; Impressions of AnywhereAmps Alpha V1.1&#10; &lt;/div&gt;&lt;/div&gt;&#10;&#10; &lt;div class="hugo-gallery__full" aria-live="polite"&gt;&lt;figure&#10; id="gallery-full-0"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/aa_alpha11/battery.jpg"&#10; alt="Image: AnywhereAmps Alpha V1.1, switched on, with battery attached"&gt;&lt;figcaption&gt;Battery attached and switched on&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-1"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/aa_alpha11/front_top.jpg"&#10; alt="Image: AnywhereAmps Alpha V1.1, front view"&gt;&lt;figcaption&gt;Front view, initial version of speaker protective grille&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-2"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/aa_alpha11/bottom.jpg"&#10; alt="Image: AnywhereAmps Alpha V1.1 bottom view"&gt;&lt;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&#10; id="gallery-full-3"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/aa_alpha11/parts.jpg"&#10; alt="Image: CNC-cut parts alongside with speaker and shell"&gt;&lt;figcaption&gt;Preparing final assembly&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-4"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/aa_alpha11/top_open.jpg"&#10; alt="Image: AnywhereAmps Alpha V1.1, a view inside"&gt;&lt;figcaption&gt;A view inside: Ampcarrier and circuits&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-5"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/aa_alpha11/side.jpg"&#10; alt="Image: Alpha V1.1&amp;#39;s body side view"&gt;&lt;figcaption&gt;Allowing the wax to dry before final assembly&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&#10;&lt;/section&gt;&#10;&#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/en/projects/aa_alpha_1_2/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/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="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; mid-high speakers, 8&amp;quot; subwoofer: very good low and mid, high highs a bit dull, weighs a lot&lt;/li&gt;&#10;&lt;li&gt;Alpha 1.2 v2: 2x15W bi-amped, 4&amp;quot; PA driver, 5.5&amp;quot; subwoofer, 2x piezo tweeters: Very balanced sound, build is pretty complex.&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/en/projects/carsubamp/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/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="Car subwoofer as a Bass Combo"&#10; title="CarSubAmp" /&gt;&#10;&lt;h2 id="project-stats"&gt;project stats&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Difficulty: beginner 1/5&lt;/li&gt;&#10;&lt;li&gt;Cost: ~50€ without battery pack&lt;/li&gt;&#10;&lt;li&gt;Time: ~2h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/carsubamp/carsubamp.jpg" alt="carsubamp"&gt;&lt;/figure&gt;&#10;The CarSubAmp was my first prototype for a really portable Bass Combo (Speaker cabinet + amplifier) that doesn&amp;rsquo;t even need a wall socket.&lt;/p&gt;&#10;&lt;h3 id="motivation"&gt;Motivation&lt;/h3&gt;&#10;&lt;p&gt;During Covid19 Pandemic, I enjoyed playing and excercising Bass outside in the Parks or on my balcony. Which is not a problem when wearing headphones and carrying a small preamp. But, there were occasions when I wanted to play along with, say, an Acoustic Guitar, or somebody would bring a Synth-App along with a MIDI Keyboard - and I was forced to use a small bluetooth speaker that neither had the output power nor the real-time feel.&lt;/p&gt;&#10;&lt;h3 id="problem"&gt;Problem&lt;/h3&gt;&#10;&lt;p&gt;My Bass amp is far from mobile. Weight: 18kg, size: huge, plus: It&amp;rsquo;s useless without a wall socket nearby.&lt;/p&gt;&#10;&lt;h3 id="dead-ends"&gt;Dead ends&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Instead of a Class-D amp I used an old Car Radio&amp;rsquo;s aux in, but it seems to have expected &amp;ldquo;Line&amp;rdquo; level which my bass couldn&amp;rsquo;t supply so the output was rarely audible. Additionally, the idle current draw was &amp;gt;0.5A so it would drain the battery all too quick.&lt;/li&gt;&#10;&lt;li&gt;I tried the power supply with a powerbank (Power-Delivery) first, but the integrated step-up converter didn&amp;rsquo;t seem to handle full-volume current surges, so the supply voltage dropped, leading to audible distortion.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="solution"&gt;Solution&lt;/h3&gt;&#10;&lt;p&gt;After some thinking, I came up with the idea of using a cordless multi tool&amp;rsquo;s Li-Ion battery that would be able to handle current surges easily, have a low weight, feature a battery level indicator and the typical protection circuits to avoid overload, short circuit, over temperature, exhaustive discharge etc. I wanted a &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; power amplifier matched to the voltage range of this battery to be as energy efficient and as lightweight&#10;as possible.&lt;/p&gt;&#10;&lt;h3 id="the-carsubamp"&gt;The &amp;ldquo;CarSubAmp&amp;rdquo;&lt;/h3&gt;&#10;&lt;p&gt;To check if the concept was viable, I took a used OEM car subwoofer assembly that normally goes into the spare wheel well, bought a 20W &lt;a href="https://www.adafruit.com/product/1752" target="_blank" rel="noopener noreferrer" class="external-link"&gt;amplifier developer board&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and after soldering some components, adding a fuse and an 6.3mm input jack, my first prototype was ready to try. &lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/carsubamp/carsubamp_explained.jpg" alt="carsubamp components explained"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;h2 id="project-summary"&gt;Project summary&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Very lightweight, &amp;lt;5kg&lt;/li&gt;&#10;&lt;li&gt;4Ohms Subwoofer assembly &amp;ldquo;ready to use&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;vented design for better speaker efficiency&lt;/li&gt;&#10;&lt;li&gt;Sound is OK, but it&amp;rsquo;s clear that the assembly is not meant to be used as a full-frequency range speaker. Muffled.&lt;/li&gt;&#10;&lt;li&gt;Playing low notes (i.e. at or below &lt;code&gt;g1&lt;/code&gt;), the system would become noticeably quieter&lt;/li&gt;&#10;&lt;li&gt;Developer board comes without preamp, thus maximum volume a bit low already&lt;/li&gt;&#10;&lt;li&gt;Under high load, vented design &amp;ldquo;Bassreflex&amp;rdquo; leads to audible hissing noise&lt;/li&gt;&#10;&lt;li&gt;Due to its small diameter, the speaker would hit maximum excursion limits when playing Hammer-ons on low strings&lt;/li&gt;&#10;&lt;li&gt;All components fixed with hot melt or tape, thus not very durable&lt;/li&gt;&#10;&lt;li&gt;Assembly is sealed with glue, no way of permanently adding a case or enclosure for battery, amp, cables, or input jacks&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Altough this design had clear disadvantages, I actually enjoyed it a lot because for the first time, I could literally play anywhere and anytime I wanted. I even caught myself on the couch with this little amp although the &amp;ldquo;real&amp;rdquo; Amp was just one room away.&lt;/p&gt;&#10;&lt;p&gt;After some months carrying this thing around, 3 re-solderings of broken connections and glueing the battery back into place again and again, I decided not to stop with this prototype.&lt;/p&gt;&#10;</description></item><item><title>ClickEncoder</title><link>https://blog.schallbert.de/en/projects/encoder/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/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="Encoder on Breadboard"&#10; title="ClickEncoder" /&gt;&#10;&lt;h2 id="project-stats"&gt;Project stats&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Difficulty: medium 3/5&lt;/li&gt;&#10;&lt;li&gt;Cost: 0€&lt;/li&gt;&#10;&lt;li&gt;Time: ~6h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="abstract"&gt;Abstract&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://raw.githubusercontent.com/isocpp/logos/64ef037049f87ac74875dbe72695e59118b52186/cpp_logo.svg" alt="C&amp;#43;&amp;#43;"&gt;&lt;/figure&gt;Library for the &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;, written in c++.&lt;/p&gt;&#10;&lt;p&gt;It is lightweight because the source code itself is a few hundred lines only, and powerful, because of the improved algorithm uses less calculations while providing a better state interpretation than the original project this is forked from. It is officially released on &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; and hosted on &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;. It contains examples both for Arduino and PlatformIO IDEs and provides unittests that can be run upon customization.&lt;/p&gt;&#10;&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;&#10;&lt;p&gt;I used the original library published by &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; at first but that one caused some compiler warnings for me, plus the encoder readings were not flawless and sometimes there was jitter or bouncing I didn&amp;rsquo;t want to happen.&lt;/p&gt;&#10;&lt;h2 id="solution"&gt;Solution&lt;/h2&gt;&#10;&lt;p&gt;As the repo looked unmaintained, I decided to fix these issues by rewriting some code. For my project at that time, I also needed a &amp;ldquo;repeat&amp;rdquo; signal when a button is pressed continuously, so I added this, too. And finally, I separated the encoder algorithm from the button handling and provided separate classes so that, if needed, any combination of button and encoder could be used, each with configurable behavior.&lt;/p&gt;&#10;&lt;h2 id="encoder-algorithm-explained"&gt;Encoder Algorithm explained&lt;/h2&gt;&#10;&lt;h3 id="api"&gt;API&lt;/h3&gt;&#10;&lt;p&gt;ClickEncoder&amp;rsquo;s API is simple and easy to understand: &lt;code&gt;service()&lt;/code&gt; runs the business logic and does the state interpretation. It has to be run regularly to check if the encoder has been moved since its last call, into which direction, how many steps, and has these values saved in an accumulator variable.&#10;&lt;code&gt;getIncrement()&lt;/code&gt; returns the encoder step changes since its last call. &lt;code&gt;getAccumulate()&lt;/code&gt; returns the sum of steps taken since startup. &lt;code&gt;getButton()&lt;/code&gt; returns the current Button state.&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-state"&gt;Encoder state&lt;/h3&gt;&#10;&lt;p&gt;The encoder uses a &lt;a href="https://en.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; to encode the steps. &lt;code&gt;getBitCode()&lt;/code&gt; converts this code transmitted via the Encoder&amp;rsquo;s hardware pins A and B to a &lt;code&gt;0...3&lt;/code&gt; notch interpretation, numbers increasing when turning right, and decreasing when turning left. This is done efficiently without use of &lt;code&gt;if()&lt;/code&gt; or other branching logic.&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;// invert result&amp;#39;s 0th bit 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;I used a trick here, so you may have to look at the code snippet above several times. The state of &lt;code&gt;PinA&lt;/code&gt; is written to bit1 of &lt;code&gt;currentEncoderRead&lt;/code&gt; by me shifting it to the left with the &lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt; operation. &lt;em&gt;But at the same time the state of the 0th bit is preserved by &lt;code&gt;|=&lt;/code&gt;&lt;/em&gt;. If I now bind &lt;code&gt;PinB&lt;/code&gt; simply by logical &lt;code&gt;&amp;amp;=&lt;/code&gt;, then I would violate the Gray code, after which &lt;code&gt;A&amp;amp;&amp;amp;B=2&lt;/code&gt; should be and not &lt;code&gt;3&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;So if &lt;code&gt;A&lt;/code&gt; and &lt;code&gt;B&lt;/code&gt; are both not set, the result remains &lt;code&gt;0&lt;/code&gt;, because &lt;code&gt;00^00=00=0&lt;/code&gt;. If only &lt;code&gt;A&lt;/code&gt; is set, but &lt;code&gt;B&lt;/code&gt; is not, the result is &lt;code&gt;11^00=11=3&lt;/code&gt;. If &lt;code&gt;B&lt;/code&gt; is set, but &lt;code&gt;A&lt;/code&gt; is not, the result is &lt;code&gt;00^01=01=1&lt;/code&gt;. If both are set, finally &lt;code&gt;11^01=10=2&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;For more information about how logical binary operators work, please refer to &lt;a href="https://blog.schallbert.de/en/projects/fireplace/#galois-lfsr-implementation"&gt;my LED fireplace project&lt;/a&gt;&lt;/p&gt;&#10;&lt;h3 id="state-interpretation"&gt;State interpretation&lt;/h3&gt;&#10;&lt;p&gt;After the bit code is read, &lt;code&gt;rawMovement&lt;/code&gt; variable is set that tells us how much the encoder has been turned between two runs of the &lt;code&gt;service()&lt;/code&gt; routine. Here you can see the time criticality because there will be aliasing errors if the encoder changes notches in a time similarly short to the service interval.&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;Here is another trick: &lt;code&gt;signedMovement&lt;/code&gt; is calculated by converting the movement into a signed integer representing &lt;code&gt;0&lt;/code&gt; as &amp;ldquo;not rotated, &lt;code&gt;-1&lt;/code&gt; as &amp;ldquo;rotated left&amp;rdquo; and &lt;code&gt;1&lt;/code&gt; as &amp;ldquo;rotated right&amp;rdquo;. This exploits the integer overflow behavior, in combination with the interpretation of signedness.&lt;/p&gt;&#10;&lt;p&gt;Finally, the accumulation is performed and the acceleration, if configured, is added to the accumulator depending on how fast the encoder is rotated.&lt;/p&gt;&#10;&lt;p&gt;To program these two methods without branching took me several evenings of grubbling and practically half a pad of squared paper. Frankly, these few lines of code have brought me to the edge of my brain&amp;rsquo;s capacity. I am just not a &amp;ldquo;real&amp;rdquo; programmer 😅&lt;/p&gt;&#10;&lt;h3 id="the-getaccumulate-method"&gt;The getAccumulate() method&lt;/h3&gt;&#10;&lt;p&gt;It&amp;rsquo;s really simple, it just returns the internal accumulated value accounted to the encoder&amp;rsquo;s configuration about how many notches make a step.&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="the-getincrement-method"&gt;The getIncrement() method&lt;/h3&gt;&#10;&lt;p&gt;Returns how much the encoder&amp;rsquo;s values have changed since its last call.&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="button-state-calculation-explained"&gt;Button state calculation explained&lt;/h2&gt;&#10;&lt;h3 id="the-handlebutton-method"&gt;The handleButton() method&lt;/h3&gt;&#10;&lt;p&gt;All button states derive from the two basic states: pressed or not pressed. This is reflected in the logic as well:&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;The button can be configured to be read less often than the encoder. If this is the case, this method will be left without any action.&lt;/p&gt;&#10;&lt;h3 id="button-pressed"&gt;Button pressed:&lt;/h3&gt;&#10;&lt;p&gt;&lt;code&gt;handleButtonPressed()&lt;/code&gt; can either return &lt;code&gt;Closed&lt;/code&gt;, &lt;code&gt;Held&lt;/code&gt;, or &lt;code&gt;LongPressRepeat&lt;/code&gt; depending on how the button is configured and how long it has been pressed already.&lt;/p&gt;&#10;&lt;h3 id="button-not-pressed"&gt;Button not pressed:&lt;/h3&gt;&#10;&lt;p&gt;&lt;code&gt;handleButtonReleased()&lt;/code&gt; can either return &lt;code&gt;Clicked&lt;/code&gt;, &lt;code&gt;Released&lt;/code&gt;, or &lt;code&gt;Doubleclicked&lt;/code&gt; depending on button configuration and click count within a certain time.&lt;/p&gt;&#10;&lt;h3 id="library-configuration"&gt;Library configuration&lt;/h3&gt;&#10;&lt;p&gt;The encoder/button detection behavior can be modified in the &lt;code&gt;encoder.h&lt;/code&gt; file using the constants shown below. The values have been tweaked already and feel natural to me, at least. They should be modified if you choose to use a different service interval than 1ms because those values all take this interval as reference.&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="use-it-in-your-project"&gt;Use it in your project!&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;The library is open source software, using the MIT license.&lt;/li&gt;&#10;&lt;li&gt;PlatformIO user? Easy, just search it in the &amp;lsquo;Libraries&amp;rsquo; view, Tags are Arduino, Encoder, Schallbert. It&amp;rsquo;s then just one more click to add it to your solution&lt;/li&gt;&#10;&lt;li&gt;Alternatively, just clone or fork the &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; and use the header/cpp file as you deem fit.&lt;/li&gt;&#10;&lt;li&gt;Contribute? Maybe you have a more efficient state calculation of the button? I&amp;rsquo;d be happy to review your pull request :)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item><item><title>CNC spoilboard</title><link>https://blog.schallbert.de/en/projects/cnc_spoilboard/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/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="Spoilboard for my CNC"&#10; title="CNC spoilboard" /&gt;&#10;&lt;h2 id="project-stats"&gt;Project stats&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Difficulty: medium 3/5&lt;/li&gt;&#10;&lt;li&gt;Cost: ~60€&lt;/li&gt;&#10;&lt;li&gt;Time: ~6h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;a href="https://blog.schallbert.de/en/cnc-spoilboard-diy/"&gt;Go to instructions&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/spoilboard/crop.jpg" alt="spoilboard: features"&gt;&lt;/figure&gt;&#10;This is how to make a versatile spoilboard for your CNC application. The design uses hammer nut slots in different angles to enable very flexible clamping. It also includes a fence that makes preparing sheet material cuts easy: workpiece zero is at the same spot every time, and clamping against the fence might already suffice so that no load in Z-direction is needed. The fence has two cutouts: One to hold the tool-length sensor, and another one at XY-zero to mitigate tool collisions during rapid positioning.&lt;/p&gt;&#10;&lt;h3 id="simple-adjustment-for-your-cnc"&gt;Simple adjustment for your CNC&lt;/h3&gt;&#10;&lt;p&gt;The dimensions I took are matching for a Basicline 0607 portal milling machine. But as I&amp;rsquo;m using a parametric CAD tool, all you need to do is to change dimensions to fit your machine, resolve some drawing quirks and you&amp;rsquo;re good to go with your own design.&lt;/p&gt;&#10;&lt;h2 id="the-idea"&gt;The idea&lt;/h2&gt;&#10;&lt;p&gt;As indicated above, the spoilboard has to meet the following criteria:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Mountable on CNC machine&amp;rsquo;s base frame (hammernuts / M5 fasteners)&lt;/li&gt;&#10;&lt;li&gt;Mount point close to XY-zero&lt;/li&gt;&#10;&lt;li&gt;T-slots for flexible mounting at 0°, 22.5°, 45°, 67.5°, 90°&lt;/li&gt;&#10;&lt;li&gt;Fence, quick-mount onto spoilboard to simplify clamping and improve repeatibility&lt;/li&gt;&#10;&lt;li&gt;Fence to hold tool-length sensor&lt;/li&gt;&#10;&lt;li&gt;Fence to provide a cutout at XY-zero&lt;/li&gt;&#10;&lt;li&gt;Shall be manufacturable on the CNC it&amp;rsquo;s designed for (multi-job if needed)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="design-phase-cad"&gt;Design phase: CAD&lt;/h2&gt;&#10;&lt;p&gt;I started with a dimensions table in Freecad and a couple of empty-sheet sketches.&lt;/p&gt;&#10;&lt;h3 id="mounting-to-base-frame"&gt;Mounting to base frame&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/spoilboard/spoilboard_mount.jpg" alt="spoilboard: top view, mount to frame"&gt;&lt;/figure&gt;&#10;In the spreadsheet, I started by both entering the base frame&amp;rsquo;s and the CNC travel range&amp;rsquo;s dimensions, the drive-in nut&amp;rsquo;s, washer&amp;rsquo;s, and fastener&amp;rsquo;s parameters.&#10;I measured the exact positions of the base frame&amp;rsquo;s t-nuts where I wanted the spoilboard to mount onto and entered them in the spreadsheet.&lt;/p&gt;&#10;&lt;p&gt;When this was done, I opened the first sketch and drew the spoilboard&amp;rsquo;s rectangle, fully constraining its dimensions with the data from the spreadsheet. I added construction lines (i.e. lines that wouldn&amp;rsquo;t show in the final drawing, but help anchor constraints, draw lines, or mark symmetry within a drawing) where the base frame&amp;rsquo;s T-nut centers are positioned.&lt;/p&gt;&#10;&lt;p&gt;I added concentric circles for washer countersinks and through-holes in evenly distributed distances. They will later hold the fasteners and their washers. I didn&amp;rsquo;t decide on their final position on the Y-axis yet, as the spoilboard&amp;rsquo;s T-nuts would interfere with some of these mounting points.&lt;/p&gt;&#10;&lt;p&gt;The CNC job for this sketch will have to be done on the &amp;ldquo;top side&amp;rdquo; of the spoilboard.&lt;/p&gt;&#10;&lt;h3 id="t-nut-slots-and-fence-mounting-points"&gt;T-nut slots and fence mounting points&lt;/h3&gt;&#10;&lt;p&gt;Now it was time to add some data to the spreadsheet again: T-nut slot parameters, angles, border width like so:&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/spoilboard/tslot_data.jpg" alt="spoilboard: t-nut slot data"&gt;&lt;/figure&gt;&#10;First I had to add construction lines on the spoilboard to mark where the CNC&amp;rsquo;s travel range. Second, I had to reserve some room for the fence. Only then could I start to draw and position the T-slots which each had different angles and lengths based on the border I wanted to keep for stability reasons.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/spoilboard/spoilboard_drawing.jpg" alt="spoilboard: t-nut slot drawing"&gt;&lt;/figure&gt;&#10;Every T-slot would get a symmetry construction line around which two rectangles reside to mark slot and rebate shoulders.&lt;/p&gt;&#10;&lt;p&gt;Then I added countersinks for bolt-in nuts that will later hold the fence on the spoilboard&amp;rsquo;s margin. In their center, I provide hole-throughs for the fasteners.&lt;/p&gt;&#10;&lt;p&gt;In a final step, I readjusted the base-frame mount sketche&amp;rsquo;s hole positions where they interfered with the T-nut slots.&lt;/p&gt;&#10;&lt;p&gt;The CNC job for this sketch will be executed on the &amp;ldquo;bottom side&amp;rdquo; of the spoilboard - the hammernuts will only work if they grip from below.&lt;/p&gt;&#10;&lt;h3 id="fence"&gt;Fence&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/spoilboard/fence.jpg" alt="spoilboard: fence"&gt;&lt;/figure&gt;&#10;Just some more data needed to be added to the spreadsheet here: Ellipsoid dimensions for the XY-zero cutout, tool-length sensor, and the fence&amp;rsquo;s witdh on the Y-axis. Its dimensions on the X-axis were predefined by the machine&amp;rsquo;s travel area already.&lt;/p&gt;&#10;&lt;p&gt;Drawing the fence was easy because I could use the &amp;ldquo;link edge&amp;rdquo; tool to import positions of lines from other sketches. The fence is an own job with an own physical part on the CNC, and it is only being cut on the &amp;ldquo;top side&amp;rdquo;.&lt;/p&gt;&#10;&lt;h2 id="planning-phase-cam"&gt;Planning phase: CAM&lt;/h2&gt;&#10;&lt;p&gt;Now came the differcult part: Instead of three jobs as indicated above, I actually needed six. This is, because the CNC has less travel range is smaller than the spoilboard&amp;rsquo;s dimensions. I was further planning with stocks that already have the correct outline dimensions so I could fit them on the machine.&lt;/p&gt;&#10;&lt;p&gt;The first part of this job was creating sinks and holes for the lower end of the spoilboard mounts. I set the Zero at the very bottom-left of the machine (X0, Y-max) so I could be sure that the relative coordinates would fit.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/spoilboard/mount_cam.jpg" alt="spoilboard: cam mount_lower"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;p&gt;For the upper end cut, topmost hole on the left was used as XY-reference zero created in the previous job. This way, I could clamp the stock more flexibly.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/spoilboard/mount_cam_up.jpg" alt="spoilboard: cam mount_upper"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;p&gt;I repeated this approach for the other two jobs.&lt;/p&gt;&#10;&lt;p&gt;For the T-nut slot job, I had to mirror the sketch and turn my stock around the Y-axis as the spoilboard&amp;rsquo;s bottom needed to be machined here. This could be done in CAM as well, but I preferred to quickly jump back into CAD for this task.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/spoilboard/tslot_cam.jpg" alt="spoilboard: cam tslot_lower"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;p&gt;For all these jobs, I used a &lt;code&gt;6mm&lt;/code&gt; 2-flute upcut wood bit with a cutter length of &lt;code&gt;21mm&lt;/code&gt;. As these were the first-ever steps I took with a CNC, I planned with very conservative parameters for this tool: &lt;code&gt;S24000rpm, F2800mm/min, Z+=6mm, XY+=85%&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;I even ran the first job &amp;ldquo;in the air&amp;rdquo; just to make sure I got everything set up right.&lt;/p&gt;&#10;&lt;h2 id="execution"&gt;Execution&lt;/h2&gt;&#10;&lt;p&gt;I scratched my head when I finally had the stock on machine bed: where could I clamp it? The sides were occupied by the portal, I have a wall at the rear of the machine&amp;hellip; So would it be sufficient to only clamp it on the front? And as I was cutting all the way through, should I add another spoilboard below my spoilboard so that I won&amp;rsquo;t accidentally cut into the machine bed?&lt;/p&gt;&#10;&lt;p&gt;I decided that these objections are justified and waited until the weekend was over so I could buy some scrap wood and more double-sided tape.&lt;/p&gt;&#10;&lt;p&gt;This is how I clamped: Standard C-clamps at the bottom, double-sided tape at the top. Worked very well.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/spoilboard/tslot.jpg" alt="spoilboard: execution"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;p&gt;Before finally mounting the frame to the spoilboard, I leveled it with a &amp;ldquo;spiral&amp;rdquo; strategy. It was enough to cut &lt;code&gt;Z=0.3mm&lt;/code&gt;. I used a &lt;code&gt;14mm&lt;/code&gt; face-mill cutter at a speed as low as &lt;code&gt;S=5500rpm&lt;/code&gt;. This job took a long time, almost 30 minutes. The result was a surface as smooth as the original stock&amp;rsquo;s.&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/spoilboard/leveling.jpg" alt="spoilboard:leveling"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;h2 id="assessment"&gt;Assessment&lt;/h2&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/spoilboard/hole.jpg" alt="very clean cut in MDF"&gt;&lt;/figure&gt;&#10;Luckily, in the end it all worked out. Look how clean the cut is! Seems like MDF and sharp endmills are good friends. Note: MDF really needs a good dust collection as it will generate breathtaking amounts of dust when machined.&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;m quite happy with the overall job quality. Every single mount point to machine bed and fence fits OK and the tool-length sensor sits snugly in its cutout.&lt;/p&gt;&#10;&lt;p&gt;There are two things to criticize: The endmill I used is very loud and emits an uncomfortable shrieking noise that I even &lt;a href="https://blog.schallbert.de/en/my-endmill-screams/"&gt;devoted a blog post&lt;/a&gt; to.&#10;Second, in my CAM tool, I didn&amp;rsquo;t care enough about milling direction. This way, the CAM decided to do one pass in conventional milling, another one in climb milling direction (it was a full slot as wide as the tool, so for the CAM it&amp;rsquo;s both climb milling, just the &amp;ldquo;side&amp;rdquo; of tool engagement changes). The difference is a very small but noticeable ridge on the fence&amp;rsquo;s edge.&lt;/p&gt;&#10;&lt;p&gt;With this spoilboard, I can process sheets as big as &lt;code&gt;620mm x 620mm&lt;/code&gt; now!&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/spoilboard/completed.jpg" alt="spoilboard jobs complete"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;p&gt;Well, one deficiency left is the tool-length sensor. It protrudes into the working area which is a problem when I want to machine large stocks and have tool changes planned: In this case, I&amp;rsquo;m using macros for measuring tool length which is why I don&amp;rsquo;t want to remove the sensor. As an iteration, I could rework the fence so it fully encloses the sensor.&lt;/p&gt;&#10;&lt;p&gt;Maybe.&lt;/p&gt;&#10;&lt;p&gt;Later.&lt;/p&gt;&#10;</description></item><item><title>Github Pages: Moving out</title><link>https://blog.schallbert.de/en/projects/move-blog-to-own-server/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/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="Github Pages: Moving out" /&gt;&#10;&lt;h2 id="project-stats"&gt;Project stats&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Difficulty: medium 3/5&lt;/li&gt;&#10;&lt;li&gt;Cost: 5-50€/Month&lt;/li&gt;&#10;&lt;li&gt;Time: ~10h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;&#10;&lt;p&gt;I decided to move &lt;a href="https://blog.schallbert.de/en/projects/thissite/"&gt;my Website&lt;/a&gt; from &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; to my own server. I have two reasons for that: First, I want the server to be situated in Germany (data protection) and second, I wanted a simple way of providing my page in two languages.&lt;/p&gt;&#10;&lt;p&gt;In addition, I might be able to realize additional wishes: Setup a miniature file server and add a Website for my small side-hustle. But those will be handled in a future post.&lt;/p&gt;&#10;&lt;h2 id="overview"&gt;Overview&lt;/h2&gt;&#10;&lt;p&gt;In this project, I&amp;rsquo;ll rent a virtual server, configure access via &lt;code&gt;SSH&lt;/code&gt; and install a container manager called &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;.&lt;/p&gt;&#10;&lt;p&gt;In one of the containers, I want an instance of &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; to provide version controls for website, and have a CI/CD&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; pipeline setup that automatically integrates and deploys my site.&lt;/p&gt;&#10;&lt;p&gt;Not unlike Github, &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; allows me to deploy the site built with &lt;code&gt;Jekyll&lt;/code&gt; via &lt;a href="https://docs.gitea.com/next/usage/actions/quickstart#use-actions" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Workflow files&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; on a &lt;code&gt;caddy&lt;/code&gt;-Server instance.&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; is not just a webserver, but also provides a &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; which does Port mapping and address resolution. In addition, &lt;code&gt;caddy&lt;/code&gt; manages my &lt;code&gt;https&lt;/code&gt; certificates easily per &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;As caddy will run in an own docker container, I have to generate and configure virtual networks so the containers can &amp;ldquo;talk&amp;rdquo; to each other.&lt;/p&gt;&#10;&lt;p&gt;Let&amp;rsquo;s get going - step by step.&lt;/p&gt;&#10;&lt;h2 id="configuring-my-server"&gt;Configuring my server&lt;/h2&gt;&#10;&lt;p&gt;The upcoming sections describe the individual steps of configuring a cloud-server for deployment of a static site, including version control and CI.&lt;/p&gt;&#10;&lt;h3 id="configure-ssh-for-the-server"&gt;configure SSH for the server&lt;/h3&gt;&#10;&lt;p&gt;Right after ordering the &amp;ldquo;Cloud-server&amp;rdquo;, I enabled SSH access and deactivated its password login.&#10;To connect from my machine, I check whether the ssh-agent is already up and running per &lt;code&gt;eval ssh-agent&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Then, I create a new key pair via&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/my-cloud-server&amp;quot;&lt;/code&gt;&lt;/p&gt;&#10;&lt;p&gt;This line creates a key pair with algorithm (&lt;code&gt;-t&lt;/code&gt;) &lt;code&gt;ed25519&lt;/code&gt;, taking a mail address as comment (&lt;code&gt;-C&lt;/code&gt;) that is then saved with filename (&lt;code&gt;-f&lt;/code&gt;) &lt;code&gt;my-cloud-server&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;The public key file &lt;code&gt;my-cloud-server.pub&lt;/code&gt; is then uploaded on the web portal of my server using the &amp;ldquo;security&amp;rdquo; tab. The resulting fingerprint is then verified with:&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;Finally, I modified the &lt;code&gt;.ssh/config&lt;/code&gt; file so that, to connect, I just need to enter &lt;code&gt;ssh server&lt;/code&gt; into my console instead of the server&amp;rsquo;s IP-Address - pretty Lazy 😀&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/my-cloud-server&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now when I enter &lt;code&gt;ssh server&lt;/code&gt; the first time, I get a prompt about a potential connection to a yet unknown host with fingerprint &lt;code&gt;xyz&lt;/code&gt; and get asked whether I&amp;rsquo;d want that to happen. I compare this fingerprint to the server&amp;rsquo;s (use provider&amp;rsquo;s Website to verify). This way I can make sure that I&amp;rsquo;m trying to connect to the correct party. Once I give my &lt;code&gt;yes&lt;/code&gt;, the fingerprint is added to the &lt;code&gt;known_hosts&lt;/code&gt; file and future connections will be made directly and without warning.&lt;/p&gt;&#10;&lt;h2 id="install-docker-"&gt;Install Docker 🐳&lt;/h2&gt;&#10;&lt;p&gt;My server runs a standard Ubuntu that supports &lt;code&gt;apt&lt;/code&gt; commands out of the box. So installing Docker per &lt;a href="https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Manual&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; is really easy: Connect to the server per SSH, type &lt;code&gt;apt install docker&lt;/code&gt;, done. I&amp;rsquo;ll be using &lt;code&gt;docker-compose&lt;/code&gt; which also auto-installs. &lt;code&gt;Compose&lt;/code&gt; interprets configuration files that define the target container and make it really easy to get them up and running.&lt;/p&gt;&#10;&lt;h2 id="caddy"&gt;Install caddy 🛒&lt;/h2&gt;&#10;&lt;p&gt;First, I create a folder in my &lt;code&gt;/opt&lt;/code&gt; directory via &lt;code&gt;mkdir caddy2&lt;/code&gt;. To have caddy run in an own container, I change into that directory and create the following &lt;code&gt;docker-compose.yml&lt;/code&gt; file:&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;This file tells docker to use latest &lt;code&gt;caddy&lt;/code&gt; release. The container is named &lt;code&gt;caddy&lt;/code&gt; and uses ports &lt;code&gt;80&lt;/code&gt; (http) and &lt;code&gt;443&lt;/code&gt; (https). Certificates are requested via &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; and the container is able to access required files (Caddyfile, data, config) on my host machine via a volume. It will also be able to access files in &lt;code&gt;/www&lt;/code&gt; folder that, in the future, will host my blog&amp;rsquo;s site data. The container is part of the &lt;code&gt;caddy-proxy&lt;/code&gt; network that has been provided externally.&lt;/p&gt;&#10;&lt;p&gt;So, to be able to run this, I&amp;rsquo;ll first have to create that network:&lt;code&gt;docker network create caddy-proxy&lt;/code&gt;. Then a quick check per &lt;code&gt;docker network ls&lt;/code&gt; shows that the network has been added:&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;Now I create a &lt;code&gt;Caddyfile&lt;/code&gt; configuration with following content:&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;Caddy is told that the content of &lt;code&gt;/www/blog&lt;/code&gt; shall be provided as a (static) file server, supporting &lt;code&gt;gzip&lt;/code&gt;-compression, available under &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;.&lt;/p&gt;&#10;&lt;h3 id="create-a-test-website-"&gt;Create a test website 🧪&lt;/h3&gt;&#10;&lt;p&gt;Before we start with the page design, we should be able to have the address resolved&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;. I log into my DNS-manager and bind a static IP address to my subdomain:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&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;My server shall be able to show something. So let&amp;rsquo;s create a minimalistic test page:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;mkdir ~/caddy2/www/blog&#10;cd ~/caddy2/www/blog&#10;nano index.html&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Hmm, Just a little something:&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;Save it, done.&lt;/p&gt;&#10;&lt;p&gt;This is how caddy&amp;rsquo;s root directory looks like now:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;*caddy_config* *caddy_data* Caddyfile docker-compose.yml *www*&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We&amp;rsquo;re ready now to create the container:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;docker compose up -d&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The Option &lt;code&gt;-d&lt;/code&gt; means &amp;ldquo;detach&amp;rdquo;, so the container is started and detached from the console, unblocking it so we can reuse it for more prompts.&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;This looks good. Now I navigate to the page:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&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;h2 id="install-gitea-"&gt;Install Gitea 🍵&lt;/h2&gt;&#10;&lt;p&gt;I need Gitea for two purposes: I want version control also for private repositories and I want to host my website with Jekyll with full CI/CD automation.&lt;/p&gt;&#10;&lt;p&gt;Like caddy, it starts with a folder:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;cd /opt&#10;mkdir gitea&#10;cd gitea&#10;nano docker-compose.yml&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I compiled Gitea&amp;rsquo;s &lt;code&gt;docker-compose.yml&lt;/code&gt; out of dfferent &lt;a href="https://docs.gitea.com/installation/install-with-docker" target="_blank" rel="noopener noreferrer" class="external-link"&gt;example files&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Here it is:&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;This is what it does: &lt;code&gt;gitea&lt;/code&gt; is member of docker networks &lt;code&gt;gitea&lt;/code&gt; and &lt;code&gt;caddy-proxy&lt;/code&gt;. The latter has been added so I can reach gitea from &amp;ldquo;the internet&amp;rdquo;. The container running the service is named &lt;code&gt;gitea&lt;/code&gt; and is automatically restarted when down. User-ID is set to &lt;code&gt;1000&lt;/code&gt; so it won&amp;rsquo;t interfere with admins or other special users. Per SSH and http, gitea can be reached under &lt;code&gt;git.schallbert.de&lt;/code&gt; on ports &lt;code&gt;22&lt;/code&gt; and &lt;code&gt;3000&lt;/code&gt; - the latter is localhost only.&lt;/p&gt;&#10;&lt;p&gt;Gitea has access to the host&amp;rsquo;s time zone and local time, it saves its logs under &lt;code&gt;/log&lt;/code&gt;and files under &lt;code&gt;/data&lt;/code&gt;. Gitea gets an &lt;code&gt;action runner&lt;/code&gt; that runs on the gitea instance. Its name is &lt;code&gt;ichlaufe&lt;/code&gt; and the config is available under &lt;code&gt;config.yml&lt;/code&gt; as a volume. The runner can access Docker daemon&amp;rsquo;s &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; to run actions, e.g. install &lt;code&gt;ubuntu-latest&lt;/code&gt; in an own container to run &lt;code&gt;Jekyll&lt;/code&gt; builds. I took the basic config for the runner from &lt;a href="https://gitea.com/gitea/act_runner/src/branch/main/examples/docker-compose" target="_blank" rel="noopener noreferrer" class="external-link"&gt;the official repository&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Gitea will only accept runners that are registered via &lt;code&gt;GITEA_RUNNER_REGISTRATION_TOKEN&lt;/code&gt;. We&amp;rsquo;ll do this at a later point in time.&lt;/p&gt;&#10;&lt;p&gt;Now we start Gitea for the first time and look at the console print:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;docker compose up&#10;[+] 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;Configure web access&lt;/h3&gt;&#10;&lt;p&gt;Now I have another subdomain added to my DNS manager:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&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;I also have to modify &lt;code&gt;Caddyfile&lt;/code&gt; so that web requests are forwarded accordingly (reverse proxy). To do this, I navigate back to the &lt;code&gt;caddy2&lt;/code&gt; folder, type &lt;code&gt;nano Caddyfile&lt;/code&gt; and add the following section:&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;Now requests to &lt;code&gt;git.schallbert.de&lt;/code&gt; are forwarded to the (local) port &lt;code&gt;3000&lt;/code&gt; of the Gitea-container that I opened in the docker compose file before.&lt;/p&gt;&#10;&lt;h3 id="configuring-gitea-per-web-page"&gt;Configuring Gitea per web page&lt;/h3&gt;&#10;&lt;p&gt;When I use my browser to navigate to the above page, I get a login screen - identical to &lt;a href="https://gitea.com/user/login" target="_blank" rel="noopener noreferrer" class="external-link"&gt;this demo&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Now I create a nice password, click &lt;code&gt;Register&lt;/code&gt; and then configure Gitea using my browser. I only do a rough config because I&amp;rsquo;ll have to modify it again later to properly add the runner.&lt;/p&gt;&#10;&lt;p&gt;When done, I create a new SSH key pair as described in the section &lt;a href="https://blog.schallbert.de/en/projects/move-blog-to-own-server/#configure-ssh-for-the-server"&gt;Configure SSH&lt;/a&gt; above with filename &lt;code&gt;-f ~/.ssh/gitea&lt;/code&gt;. It is uploaded and verified under &lt;code&gt;Settings-&amp;gt;SSH/GPG Keys&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Now I can create another entry on my local machine&amp;rsquo;s &lt;code&gt;.ssh/config&lt;/code&gt; file:&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;Let&amp;rsquo;s check if this works:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;ssh gitea&#10;Host &amp;#39;git.schallbert.de&amp;#39; is known and matches the ED25519 host key.&#10;debug1: Found key in /home/schallbert/.ssh/known_hosts&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Perfect.&lt;/p&gt;&#10;&lt;h3 id="complete-gitea-config-via-appini"&gt;Complete Gitea config via &lt;code&gt;app.ini&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;As the configuration is not complete, I shut down the container again:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;docker compose down&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;and then navigate to &lt;code&gt;/gitea/gitea/conf&lt;/code&gt; and modify the &lt;code&gt;app.ini&lt;/code&gt; just a little:&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; &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;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;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_OPENID_SIGNUP&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;[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;Here it is important to make sure that &lt;code&gt;ROOT_URL&lt;/code&gt; and port configuration match the one specified in &lt;code&gt;docker-compose.yml&lt;/code&gt;. I configure its database (sqlite3) and make it available to Gitea. Plus, I deactivate Giteas ability to register new users &lt;code&gt;DISABLE_REGISTRATION = true&lt;/code&gt; and the option to sign up/in via openID &lt;code&gt;ENABLE_OPENID_SIGNIN = false ENABLE_OPENID_SIGNUP = false&lt;/code&gt;. Finally, I enable Gitea Actions &lt;code&gt;ENABLED = true&lt;/code&gt; and save the file.&lt;/p&gt;&#10;&lt;p&gt;Now I can restart the container:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;docker compose up&#10;[+] Running 4/4&#10; ✔ Network gitea_default Created 0.1s &#10; ✔ Network gitea_gitea Created 0.0s &#10; ✔ Container gitea Started 0.0s &#10; x Container gitea-runner-1 Error 0.1s &#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Very well, Gitea is up and running. In a last step, I configure the Action Runner.&lt;/p&gt;&#10;&lt;h2 id="setup-action-runner-"&gt;Setup Action Runner 🏃&lt;/h2&gt;&#10;&lt;p&gt;I use the official &lt;a href="https://docs.gitea.com/usage/actions/act-runner" target="_blank" rel="noopener noreferrer" class="external-link"&gt;documentation&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; provided by Gitea for inspiration.&lt;/p&gt;&#10;&lt;p&gt;I login to Giteas Web surface and see that activating Actions in the &lt;code&gt;app.ini&lt;/code&gt; added another menu: &lt;code&gt;Settings-&amp;gt;Actions&lt;/code&gt;.&#10;Here I click &lt;code&gt;Create new Runner&lt;/code&gt; und copy its &lt;code&gt;REGISTRATION TOKEN&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Then, I re-open Gitea&amp;rsquo;s &lt;code&gt;docker-compose.yml&lt;/code&gt; and add the following lines &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;as recommended in the doc page&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-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;I restart the container with &lt;code&gt;docker compose down &amp;amp;&amp;amp; docker compose up -d&lt;/code&gt; and get a &amp;ldquo;started&amp;rdquo; message for the runner in the logs:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;✔ Network gitea_default Created 0.1s &#10;✔ Network gitea_gitea Created 0.0s &#10;✔ Container gitea Started 0.0s &#10;✔ Container gitea-runner-1 Started 0.1s &#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Great, let&amp;rsquo;s have a look at the web page:&#10;&lt;figure class="media-frame media-frame--center"&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;&lt;/p&gt;&#10;&lt;p&gt;Now we have to know if the runner can do something for us.&lt;/p&gt;&#10;&lt;h3 id="runner-functional-test"&gt;Runner functional test&lt;/h3&gt;&#10;&lt;p&gt;Once again, I have a look at the &lt;a href="https://blog.gitea.com/hacking-on-gitea-actions/#use-actions" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Runner&amp;rsquo;s documentation&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. To check, I create a new Repository on the web surface, name it &lt;code&gt;runner-test&lt;/code&gt;, and add a file to &lt;code&gt;/.gitea/workflows/01-test.yml&lt;/code&gt; containing the recommended runner check.&lt;/p&gt;&#10;&lt;p&gt;Commit, push, and - nothing.&lt;/p&gt;&#10;&lt;p&gt;Oh, the workflow starts &lt;code&gt;on: [push]&lt;/code&gt;. This cannot work when it has just been uploaded itself.&lt;/p&gt;&#10;&lt;p&gt;Well, let&amp;rsquo;s &lt;code&gt;touch&lt;/code&gt; another random file in the repository. Commit, push, result:&#10;&lt;figure class="media-frame media-frame--center"&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;&lt;/p&gt;&#10;&lt;p&gt;Fantastic! 🥳&lt;/p&gt;&#10;&lt;h2 id="interim-conclusion"&gt;Interim conclusion&lt;/h2&gt;&#10;&lt;p&gt;In this article, I pretended that the road to a working server setup was straight and without obstacles. Of course, that is not entirely true. It was really simple to run images in Docker, but to have them communicate turned out less straight-forward. I also had quite some problems with &lt;code&gt;Caddyfile&lt;/code&gt; as it is hard to debug and I couldn&amp;rsquo;t see what I was doing wrong.&lt;/p&gt;&#10;&lt;p&gt;For instance, I was able to ping the website on my host but couldn&amp;rsquo;t reach it from the outside. So I knew it is the caddy config to modify.&lt;/p&gt;&#10;&lt;p&gt;Root cause was missing the Caddyfile in the Docker volume list and forgetting a leading &lt;code&gt;/&lt;/code&gt; in the target volume folder.&lt;/p&gt;&#10;&lt;p&gt;On the other hand, configuring Gitea was much easier than I thought. Even complex process of registering the runner and its routing through Docker worked at first try.&lt;/p&gt;&#10;&lt;p&gt;Now I have a quick and lightweight server that will be able to host my blog with ease.&lt;/p&gt;&#10;&lt;p&gt;Next steps - creating a build process for my site - is tried &lt;a href="https://blog.schallbert.de/en/gitea-action-runner-native-jekyll/"&gt;in this post&lt;/a&gt;, but then replaced with native Jekyll &lt;a href="https://blog.schallbert.de/en/gitea-action-runner-jekyll-dockerimage/"&gt;over here&lt;/a&gt;.&lt;/p&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Update: Server architecture&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2024-01-27T00:00:00Z"&gt;&#10; 2024-01-27&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; I have now added several peripheral functions to the server. I have taken care of &lt;a href="https://blog.schallbert.de/en/server-protection/"&gt;security and backups&lt;/a&gt; as well as &lt;a href="https://blog.schallbert.de/en/server-auto-upgrade/"&gt;automatic updates&lt;/a&gt;, whereby the encapsulation by Docker has cost me a lot of additional time and some headache. Even if not all aspects are yet fully covered (notifications, for example, are still an open issue), I am getting noticeably closer to my goal of creating a largely autonomous system.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;p&gt;Ultimately, I want to get back to creating content. And not spend any more time than necessary on all the other stuff.&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 &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; cares for automatic build, verification, and deployment of my web page.&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 = &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;, a distributed hierarchical system for name resolution of websites. It exists because it&amp;rsquo;s much less straight forward to enter an IPV6 address like &lt;code&gt;2a02:ec80:300:ed1a:0:0:0:1&lt;/code&gt; into your web browser than &lt;code&gt;en.wikipedia.org&lt;/code&gt;.&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>GoogleTest Demo</title><link>https://blog.schallbert.de/en/projects/platformio_gtestgmock/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/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="Googletest&amp;#39;s successful test results on the example program"&#10; title="GoogleTest Demo" /&gt;&#10;&lt;h2 id="project-stats"&gt;Project stats&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Difficulty: medium 3/5&lt;/li&gt;&#10;&lt;li&gt;Cost: 0€&lt;/li&gt;&#10;&lt;li&gt;Time: ~2h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;&#10;&lt;p&gt;It is nice to have a powerful unit test framework at hand when developing more complex projects, possibly saving a major amount of time debugging (especially on-Target debugging in the embedded world can be really time-consuming), improving code quality and possibly boosting insights to APIs and architecture for others.&lt;/p&gt;&#10;&lt;h2 id="the-embedded-world"&gt;The embedded world&lt;/h2&gt;&#10;&lt;p&gt;Traditionally, microcontrollers were limited on the memory side. In the past, they neither had much RAM nor PROGMEM so the code I wrote was mostly in lean &lt;code&gt;C&lt;/code&gt;, and configuration took place in lists of &lt;code&gt;#define&lt;/code&gt; that wouldn&amp;rsquo;t weigh much because the preprocessor would do the work; not the µC at runtime.&#10;My unit tests were alyways bound to the hardware they were running on - take &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; as a well-known example - but now I wanted to try something new:&#10;Write Object-oriented style code, and get the business logic tested independently of the hardware. I selected the &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 and I wanted a seamless integration into my workflow with &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; and its extension &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;.&lt;/p&gt;&#10;&lt;p&gt;Until recently, the only way to do this was to run &lt;em&gt;GoogleTest&lt;/em&gt; in parallel using the &lt;em&gt;GNU Compiler Collection&lt;/em&gt; and have it controlled by PlatformIO. Recently, however, &lt;em&gt;GoogleTest&lt;/em&gt; can be used directly as a &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;. Therefore, I&amp;rsquo;ll cover both options here.&lt;/p&gt;&#10;&lt;h2 id="using-googletest-directly-in-platformio"&gt;Using GoogleTest directly in PlatformIO&lt;/h2&gt;&#10;&lt;p&gt;The installation is particularly easy here:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Clone my 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;.&lt;/li&gt;&#10;&lt;li&gt;Open a console window in the repository folder and switch to the corresponding branch using &lt;code&gt;git checkout googletest-native&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Follow the instructions in the repository&amp;rsquo;s &lt;em&gt;README&lt;/em&gt; file and test the setup.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;This is what it looks like when the tests have been successfully run:&#10;&lt;figure class="media-frame media-frame--center"&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;&lt;/p&gt;&#10;&lt;h2 id="using-googletest-with-gcc"&gt;Using GoogleTest with GCC&lt;/h2&gt;&#10;&lt;p&gt;You can install the required software yourself (even on a 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; computer) to get started with GoogleTest.&lt;/p&gt;&#10;&lt;h3 id="download-and-install-mingw"&gt;Download and install MinGW&lt;/h3&gt;&#10;&lt;p&gt;See &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;this thread&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; in the PlatformIO forum if you have any questions about the following steps.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Unix users: Skip the steps up to this point, as you probably only need the &lt;code&gt;libpthreadgc&lt;/code&gt; libraries. Check for the presence of all libraries (but omit the &lt;code&gt;mingw32-&lt;/code&gt; prefix) and install any missing ones. Make sure the &lt;code&gt;PATH&lt;/code&gt; variable is set correctly.&lt;/li&gt;&#10;&lt;li&gt;Windows users:&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Download MinGW, e.g., from &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;Install by running &lt;code&gt;mingw-get-setup.exe&lt;/code&gt;. If you like graphical user interfaces, leave the corresponding box checked. I &lt;em&gt;do not&lt;/em&gt; recommend changing the installation directory. If you do, you&amp;rsquo;ll need to remember the new path. &lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/platformio_gtestgmock/mingw_install.jpg" alt="MinGW install"&gt;&lt;/figure&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="configure-mingw"&gt;Configure MinGW&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;For GoogleTest to run, you&amp;rsquo;ll require additional packages to be installed:&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;mingw32-gcc-g++&lt;/code&gt; (of course!)&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;To install these,&#10;&lt;ul&gt;&#10;&lt;li&gt;If you use the GUI: search libraries under &amp;ldquo;MinGW Standard Libraries&amp;rdquo;, add/activate/install them one by one.&lt;/li&gt;&#10;&lt;li&gt;Command Line Interpreter users: type &lt;code&gt;mingw-get install &lt;/code&gt; and then add the package name. Easy.&#10;If you get an error message saying that some of those do already exist, that&amp;rsquo;s good for you!&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;Now you need to add MinGW&amp;rsquo;s &lt;code&gt;\bin&lt;/code&gt; folder to your system path variables so PlatformIO can find them later. If you didn&amp;rsquo;t hand-modify it, it should be &lt;code&gt;C:\MinGW\bin&lt;/code&gt;.&#10;&lt;ul&gt;&#10;&lt;li&gt;GUI fans: follow &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;this guide&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;CLI: type &lt;code&gt;set PATH=%PATH%;C:\MinGW\bin&lt;/code&gt;, check if it worked with &lt;code&gt;echo %PATH:;=&amp;amp;echo.%&lt;/code&gt;.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Congratulations! You have installed MinGW, set its environment variables to be found more easily by third party applications, and configured it to work with GoogleTest.&lt;/p&gt;&#10;&lt;h3 id="configure-platformio"&gt;Configure PlatformIO&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;After setting the &lt;code&gt;PATH&lt;/code&gt; variable, you&amp;rsquo;ll have to restart VScode.&lt;/li&gt;&#10;&lt;li&gt;To make PlatformIO correctly link to the compiler you just install, make it use the &lt;code&gt;native&lt;/code&gt; environment where your compiler sits. Open a PlatformIO terminal within one of your projects and type &lt;code&gt;pio platform install native&lt;/code&gt; &lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/platformio_gtestgmock/install_native.jpg" alt="how to install PIO native"&gt;&lt;/figure&gt;&lt;/li&gt;&#10;&lt;li&gt;Two choices:&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Follow through this tutorial and use my example project as described &lt;a href="https://blog.schallbert.de/en/projects/platformio_gtestgmock/#writing-tests"&gt;below&lt;/a&gt;. You can safely skip the upcoming section as that project comes pre-configured and is open for your modifications as you have basic hardware abstraction interfaces at the ready right from the start.&lt;/li&gt;&#10;&lt;li&gt;Take your own project and modify its &lt;code&gt;Platformio.ini&lt;/code&gt; so that GoogleTest can be run.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;ol start="4"&gt;&#10;&lt;li&gt;This what you should add to your &lt;code&gt;Platformio.ini&lt;/code&gt;:&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;For the environment &lt;code&gt;desktop&lt;/code&gt; (= your computer without target), it uses the &lt;code&gt;native&lt;/code&gt; compiler which is GCC/G++ that you installed and set the &lt;code&gt;PATH&lt;/code&gt; for. GoogleTest uses C++11 standard and threading which you have to add to the build flags.&#10;Of course, the build depends on the &lt;code&gt;googletest&lt;/code&gt; library and you want to ignore all files/folders that contain source or header files using hardware-related commands or headers. Library compatibility mode has to be switched off for the Library Dependency Finder to include &lt;code&gt;googletest&lt;/code&gt;.&lt;/p&gt;&#10;&lt;ol start="5"&gt;&#10;&lt;li&gt;(Optional) In case you like using GUI buttons better than typing a line into your console to start the tests, create a custom &lt;a href="https://docs.platformio.org/en/latest/projectconf/build_configurations.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;configuration&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="running-tests"&gt;Running Tests&lt;/h3&gt;&#10;&lt;p&gt;For a quicker start, you can clone my example repository (on branch &lt;em&gt;main&lt;/em&gt;). It contains all the necessary elements to test whether &lt;code&gt;gTest&lt;/code&gt; and &lt;code&gt;gMock&lt;/code&gt; are working correctly. This allows you to immediately verify whether the installation was successful.&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Unzip the zip file or use SSH to clone the repository.&lt;/li&gt;&#10;&lt;li&gt;Open the PlatformIO extension home page, click &amp;ldquo;Open Project,&amp;rdquo; and select the folder.&lt;/li&gt;&#10;&lt;li&gt;To run the unit tests, open a PlatformIO terminal and enter &lt;code&gt;pio test -vvv -e desktop -f test_desktop&lt;/code&gt; (-vvv = verbose -e = environment, -f = filter). The parameters are required because the tests can only be run on the desktop environment, not on the microcontroller.&lt;/li&gt;&#10;&lt;li&gt;After a while, the message shown below should appear.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;figure class="media-frame media-frame--center"&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;p&gt;Great! Unit tests within PlatformIO have been built and executed!&lt;/p&gt;&#10;&lt;h2 id="tips-and-tricks"&gt;Tips and Tricks&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt; that your &lt;code&gt;PlatformIO_gTestgMock/.pio/build/desktop&lt;/code&gt; folder now contains an executable called &lt;code&gt;program.exe&lt;/code&gt;. If you run this from the CLI of your choice, you should see the same result as in the above image. You might want to choose this option more frequently once you have more tests that not only fail but crash the program on execution, and to debug tests (and not the user code), as PlatformIO&amp;rsquo;s CLI tends to hang or miss out the reason for why the test crashed. You might also want to have the stack trace printed in these cases although this would really go far beyond scope of this exercise.&lt;/p&gt;&#10;&lt;p&gt;From here,&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;you might want to continue reading below for a deeper understanding of the repository contents and how GoogleTest works&lt;/li&gt;&#10;&lt;li&gt;you could go practice writing tests by adding more cases, e.g. to verify if the &lt;code&gt;LOW&lt;/code&gt; state is looped through correctly&lt;/li&gt;&#10;&lt;li&gt;You could also enhance the interface of these groundworks, e.g. by adding the &lt;code&gt;analogRead()&lt;/code&gt; function to the hardware abstraction layer&lt;/li&gt;&#10;&lt;li&gt;You could write your project logic on top of this, cleanly cut out the hardware dependencies, and enjoy the ability for easy porting of your project to different hardware devices and manufacturers&lt;/li&gt;&#10;&lt;li&gt;Or do this other brilliant stuff that you were up to before you were diverted by this tutorial.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="repository-contents-explained"&gt;Repository contents explained&lt;/h2&gt;&#10;&lt;p&gt;The following sections cover most of the files in the repository and explain what they do in detail. The implemented business logic is minimal and should be seen as example code - to create the least confusion possible when dealing with this complex matter. For a real-life project that uses all of the patterns illustrated on this page, please refer to my &lt;a href="https://blog.schallbert.de/en/projects/tonuino/"&gt;Tonuino&lt;/a&gt; page.&lt;/p&gt;&#10;&lt;h3 id="hardware-abstraction-layer-interface"&gt;Hardware abstraction layer interface&lt;/h3&gt;&#10;&lt;p&gt;Let&amp;rsquo;s have a look at the bespoke interface and how it&amp;rsquo;s structured.&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;The Hal_IF class has two methods which are both &lt;code&gt;virtual&lt;/code&gt;. This means that a client class using this interface can call the method, and is thus &amp;ldquo;auto-forwarded&amp;rdquo; to the concrete implementation of the method. The &lt;code&gt;= 0;&lt;/code&gt; makes these methods &lt;em&gt;pure virtual&lt;/em&gt;, so overriding them with a derived class is not optional but a &lt;em&gt;must&lt;/em&gt;. This is what we want here, because if this method wasn&amp;rsquo;t overridden, it wouldn&amp;rsquo;t have any behavior.&#10;Note that the destructor of an interface class &lt;a href="https://stackoverflow.com/questions/3628529/should-c-interfaces-have-a-virtual-destructor" target="_blank" rel="noopener noreferrer" class="external-link"&gt;almost always should be virtual&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, too.&lt;/p&gt;&#10;&lt;h3 id="loopinout"&gt;LoopInOut&lt;/h3&gt;&#10;&lt;p&gt;The logic under test. Note that it does not directly access the input/output hardware of the µC, but the &lt;code&gt;m_hal&lt;/code&gt; variable which is an implementation of the &lt;code&gt;Hal_IF&lt;/code&gt; interface at runtime. It is passed into the class by reference (marked by the &lt;code&gt;&amp;amp;&lt;/code&gt; at the type declaration) upon object construction&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;This is enabling the real magic of gMock: as the client code works with an interface, I can hand over the &amp;ldquo;real&amp;rdquo; implementation (of the hardware access) at chip runtime, but for the test I can hand over a &amp;ldquo;mock&amp;rdquo; implementation which I have control over at the time the test is running.&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; again is really simple. It reads the input pin&amp;rsquo;s state and maps it to the output pin. It returns the input pin state to the caller.&lt;/p&gt;&#10;&lt;h3 id="mock-hal"&gt;Mock Hal&lt;/h3&gt;&#10;&lt;p&gt;GoogleTest uses macros to define behavior. The mock class is also deriving from the interface and just provides declarations of the interface method it overrides. There&amp;rsquo;s no behavior involved just yet.&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="the-test-fixture"&gt;The test fixture&lt;/h4&gt;&#10;&lt;p&gt;The test fixture contains common data and behavior for all test cases within a test suite. Test suite name = class name, and this exact name has to be reused in the tests that should belong to this suite.&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;Thus, every test has some data prepared it can use: the class under test is set up as a pointer with a mocked implementation of its dependency.&lt;/p&gt;&#10;&lt;h3 id="writing-tests"&gt;The actual test&lt;/h3&gt;&#10;&lt;p&gt;The test is defined by the &lt;code&gt;TEST_F&lt;/code&gt; macro meaning &amp;ldquo;test with fixture&amp;rdquo; that expects test suite and test names.&#10;The &lt;code&gt;ON_CALL&lt;/code&gt; macro is a command to gMock providing behavior to the mocked class how to behave when &lt;code&gt;digitalRead(_)&lt;/code&gt; is called with any input (underscore = any): It shall return true by default.&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;Then there&amp;rsquo;s an expectation expressed on the mocked class, stating that &lt;code&gt;digitalWrite&lt;/code&gt; is called with input values of &lt;code&gt;(2, true)&lt;/code&gt; which are also checked for correctness.&lt;/p&gt;&#10;&lt;p&gt;The last statement actually calls the implementation so that the expecations can be verified.&lt;/p&gt;&#10;&lt;h3 id="hardware-behavior-implementation"&gt;Hardware behavior implementation&lt;/h3&gt;&#10;&lt;p&gt;As &lt;code&gt;main.cpp&lt;/code&gt; accesses the hardware, there has to be some kind of interface implementation for this.&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;The header states that &lt;code&gt;Hal&lt;/code&gt; derives from &lt;code&gt;Hal_IF&lt;/code&gt; and overrides both of its methods.&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;Note that only the source file actually includes the &lt;code&gt;Arduino.h&lt;/code&gt; to access the hardware. So the dependency to hardware functions is very limited and buried in interface implementations that can be selected by the program&amp;rsquo;s structure. While &lt;code&gt;main.cpp&lt;/code&gt; will want to create an object of the above implementation, the test will want to instead hand over a mock.&lt;/p&gt;&#10;&lt;h2 id="gtest-key-concepts"&gt;Gtest key concepts&lt;/h2&gt;&#10;&lt;p&gt;Google&amp;rsquo;s official &lt;a href="http://google.github.io/googletest/primer.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;github page on GoogleTest&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; will help you a lot, not only for getting started&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;. The gmock Cookbook is an excellent guide that I have been using over the months as my tests became more complex hard to write, and the gmock Cheat Sheet will help you when you lost track of how to write a test for this and that special problem.&lt;/p&gt;&#10;&lt;h3 id="dependency-injection"&gt;Dependency Injection&lt;/h3&gt;&#10;&lt;p&gt;Dependency Injection is the basic mechanics you need to understand to make gMock work. Let me draw an image for you to make it more apparent: &lt;figure class="media-frame media-frame--center"&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;The important concept is, that the interface is merely a placeholder for the implementation of the client classes&amp;rsquo; dependency. At runtime, e.g. when the host creates the client object, it will handover (i.e. inject) the implementation of the dependency into the client which then can call any of its methods, as their signatures are known at compile time due to the interface that the client is using.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;The mock now merely is an implementation of the interface specialized for testing.&lt;/li&gt;&#10;&lt;li&gt;The production implementation is used in all other contexts, it features the &amp;ldquo;real&amp;rdquo; behavior of that class.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="hardware-abstraction-layer"&gt;Hardware Abstraction Layer&lt;/h3&gt;&#10;&lt;p&gt;Yep, well, this might be one of the tricky bits when working with embedded software.&#10;At some point, it boils down to outside world interaction, be it with General Purpose Input Output (GPIO) pins, accessing one of the myriad of hardware bus systems like I2C, UART, SPI, CAN to control external hardware, or simply to use system internals like the EEPROM - that&amp;rsquo;s where the trouble begins.&lt;/p&gt;&#10;&lt;p&gt;Gtest runs on GCC/G++ compiler which just doesn&amp;rsquo;t know anything what the specialized compiler of that particular chip knows. It wouldn&amp;rsquo;t even understand the header files. Now, there are two options around this issue:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Write fake headers for each µC-specific thing that can be used by GCC/G++&lt;/li&gt;&#10;&lt;li&gt;Cut! At! The! Interface! to any µC-specific thing using an (Hardware) Abstraction Layer so your production environment uses the chip hardware as it normally does, but the test environment uses mocks instead.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;There are quite some advantages for the second approach:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;It&amp;rsquo;s clean. It will force you to not cross abstraction layers which should not be crossed.&lt;/li&gt;&#10;&lt;li&gt;It&amp;rsquo;s easy. You can test your business logic without even programming your µC! Just mock the inputs and intercept the outputs, write some tests and run them in the terminal.&lt;/li&gt;&#10;&lt;li&gt;It&amp;rsquo;s portable. Exchange the implementation of your abstraction layer, and you&amp;rsquo;re almost ready to execute on a different controller family or even on another manufacturer&amp;rsquo;s chips.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="drawbacks"&gt;Drawbacks&lt;/h3&gt;&#10;&lt;p&gt;Hmm, there must be some shadow somewhere. Right you are. In my opinion, these are the main disadvantages when getting everything ready to be tested without hardware attached:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/platformio_gtestgmock/memoryfull.jpg" alt="not much memory left..."&gt;&lt;/figure&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Memory. Both ROM and RAM will be cluttered more easily when you have a massive amount of classes and vtables flying around.&lt;/li&gt;&#10;&lt;li&gt;Ownership. Injecting dependencies separates ownership from useage of an object, and you need to make up your mind how to organize both.&lt;/li&gt;&#10;&lt;li&gt;3rd party libaries. You&amp;rsquo;ll have to create an interface header, an &lt;a href="https://refactoring.guru/design-patterns/adapter/cpp/example" target="_blank" rel="noopener noreferrer" class="external-link"&gt;adapter implementation&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, and a mock for each library that uses &amp;ldquo;chip-internals&amp;rdquo;.&lt;/li&gt;&#10;&lt;li&gt;Complexity. As your objects are injected via an interface, you&amp;rsquo;ll have to use call-by-reference or hand over by pointer 😨 now. The former is additionally limited because it will only work for constructor injection, not for any other kind (method injection, setter injection, etc.)&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="some-more-tips"&gt;Some more tips&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Keep everything &amp;ldquo;behind&amp;rdquo; your abstraction layer as simple as possible: You can only test and debug this stuff on target. Example: each method only executes one command (as 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;my example code&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;).&lt;/li&gt;&#10;&lt;li&gt;Create a loader class or a &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; that holds all objects to be injected and can spit them out on request.&lt;/li&gt;&#10;&lt;li&gt;Make up your mind about the drawbacks mentioned &lt;a href="https://blog.schallbert.de/en/projects/platformio_gtestgmock/#drawbacks"&gt;above&lt;/a&gt; and how you create your low-level Architecture to accomodate for this.&lt;/li&gt;&#10;&lt;li&gt;Don&amp;rsquo;t hesitate to create more than one level of interfaces. It might be a good idea to use this pattern not only for the direct hardware interaction, but also in between software modules, which then can be tested in a higher granularity so your overall progress might even be quicker versus one big ball of classes you can just control via mocks in the Hardware Abstraction Layer.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="tough-stuff"&gt;Tough Stuff!&lt;/h3&gt;&#10;&lt;p&gt;You earn a break now. This was much information to deal with. Take your time. Contact me on this topic&amp;rsquo;s &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; page if you feel something is missing, incorrect, to be improved, or just exactly what you needed right now.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Schallbert&lt;/strong&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;Installation under Linux is actually more easy, as &lt;code&gt;gcc&lt;/code&gt; and &lt;code&gt;g++&lt;/code&gt; come already pre-installed. So even if they do not feature the correct packages yet to support GoogleTest, their error message will be of much better help than the ones you get on Windows when some libraries are missing&amp;hellip; 🙃&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;This is also called &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;, a subtype of &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; which is a means to achieve &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;. I have prepared some links for you to continue reading, it is a rich topic that whole books have been written about.`&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;By The way: the googletest site is also static like mine and it uses Jekyll as well 👋 &amp;hellip;do you like it? I&amp;rsquo;d like to see your opinion &lt;a href="https://github.com/Schallbert/schallbert.github.io/discussions" target="_blank" rel="noopener noreferrer" class="external-link"&gt;on my discussions page&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&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>Legal / Imprint</title><link>https://blog.schallbert.de/en/legal/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/legal/</guid><description type="html">&#10;&lt;h2 id="legal-notice"&gt;Legal notice&lt;/h2&gt;&#10;&lt;h3 id="address"&gt;Address&lt;/h3&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/address.jpg" alt="summonable address"&gt;&lt;/figure&gt;&#10;&lt;h3 id="contact"&gt;Contact&lt;/h3&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/contact.jpg" alt="phone and mail"&gt;&lt;/figure&gt;&#10;&lt;h3 id="editors"&gt;Editors&lt;/h3&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/address.jpg" alt="summonable address"&gt;&lt;/figure&gt;&#10;&lt;h3 id="source"&gt;Source&lt;/h3&gt;&#10;&lt;p&gt;&lt;a href="https://www.e-recht24.de/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;e-recht24.de&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;h2 id="dispute-settlement"&gt;Dispute settlement&lt;/h2&gt;&#10;&lt;p&gt;I&amp;rsquo;m neither willing nor obliged to participate in a dispute settlement hearing.&lt;/p&gt;&#10;&lt;h2 id="liability"&gt;Liability&lt;/h2&gt;&#10;&lt;p&gt;Did I make a mistake or forget anything?&lt;/p&gt;&#10;&lt;p&gt;Errare humanum est. I&amp;rsquo;m trying to run this website according to the latest legal requirements. Should anything on here be non-compliant to these regulations, I humbly request for an informal note.&#10;I&amp;rsquo;ll promptly tackle valid complaints to remove deficiencies.&lt;/p&gt;&#10;&lt;h3 id="contents"&gt;Contents&lt;/h3&gt;&#10;&lt;p&gt;As a service provider, I am liable for my own content on these pages in accordance with Section 7(1) of the German Telemedia Act (TMG) and general legislation. However, pursuant to Sections 8 to 10 of the TMG, I am not obliged, as a service provider, to monitor third-party information that is transmitted or stored, or to investigate circumstances that indicate unlawful activity.&lt;/p&gt;&#10;&lt;p&gt;Obligations to remove or block the use of information in accordance with general laws remain unaffected by this. However, liability in this regard is only possible from the time I become aware of a specific legal infringement. Should I become aware of any such infringements, I will, of course, remove the content in question immediately.&lt;/p&gt;&#10;&lt;h2 id="disclaimer"&gt;Disclaimer&lt;/h2&gt;&#10;&lt;p&gt;As a service provider, I&amp;rsquo;m committed to meet German law for the contents I author. I am not obliged to monitor transmitted or saved contents of third parties or to research for potentially illicit behavior.&#10;My Website contains links / references to third party websites over which I have no control. That&amp;rsquo;s why I cannot take any responsibility for their content. I have checked third party links for legal compliance at the time of linkage and couldn&amp;rsquo;t find any illicit behavior.&lt;/p&gt;&#10;&lt;h2 id="copyright"&gt;Copyright&lt;/h2&gt;&#10;&lt;p&gt;The contents I prepare for this website are subject to German copyright. Copies, modifications, and usage beyond this require my written approval. Downloads and copies of this Website are allowed for non-commercial, private use.&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;m trying hard to mark third party content and to obtain written consent prior to their usage. Should you still spot a copyright violation, please drop me an informal note. I&amp;rsquo;ll be happy to make my website even more transparent!&lt;/p&gt;&#10;</description></item><item><title>MobFobAmp</title><link>https://blog.schallbert.de/en/projects/mobfobamp/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/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="MobFobAmp Unfolded"&#10; title="MobFobAmp" /&gt;&#10;&lt;p&gt;A second prototype of my mobile Instrument Amplifier/Cabinet combo with foldable design. &lt;code&gt;28l&lt;/code&gt; cabinet with &lt;code&gt;17cm&lt;/code&gt; woofer. &lt;code&gt;15W&lt;/code&gt; amplifier power. &lt;code&gt;7.3kg&lt;/code&gt; incl. battery. Especially suitable for Bass Guitar, Acoustic guitar, Organs.&lt;/p&gt;&#10;&lt;h2 id="project-stats"&gt;Project stats&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Difficulty: challenging 4/5&lt;/li&gt;&#10;&lt;li&gt;Cost: ~140€ without battery pack&lt;/li&gt;&#10;&lt;li&gt;Time: ~15h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="impressions"&gt;Impressions&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&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-0"&#10; aria-label="Image: amp folded, flat. Enclosure is less than 10cm strong, the speaker back protrudes by 8cm."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/mobfobamp/folded_lying.jpg"&#10; alt="Image: amp folded, flat. Enclosure is less than 10cm strong, the speaker back protrudes by 8cm."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-1"&#10; aria-label="Image: amp folded, standing by itself."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/mobfobamp/folded_standing.jpg"&#10; alt="Image: amp folded, standing by itself."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-2"&#10; aria-label="Image: amp unfolded, side view, the speaker chassis protrudes from the speaker face."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/mobfobamp/unfolded_side.jpg"&#10; alt="Image: amp unfolded, side view, the speaker chassis protrudes from the speaker face."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-3"&#10; 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&#10; src="https://blog.schallbert.de/assets/images/mobfobamp/unfolded_top.jpg"&#10; alt="Image: amp unfolded, top view. An 18mm thick beech panel keeps the enclosure shut and is sturdy enough to be sat upon."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-4"&#10; aria-label="Image: amp unfolded, front view. Battery and amplifier board are visible at the combo&amp;#39;s front."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/mobfobamp/unfolded.jpg"&#10; alt="Image: amp unfolded, front view. Battery and amplifier board are visible at the combo&amp;#39;s front."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-5"&#10; aria-label="Image: view inside the amp: Nothing but air and a strengthening panel."&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/mobfobamp/inside_spacer.jpg"&#10; alt="Image: view inside the amp: Nothing but air and a strengthening panel."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;/div&gt;&lt;div class="hugo-gallery__caption"&gt;&#10; Some images of my fold-flat bass combo 'MobFobAmp'.&#10; &lt;/div&gt;&lt;/div&gt;&#10;&#10; &lt;div class="hugo-gallery__full" aria-live="polite"&gt;&lt;figure&#10; id="gallery-full-0"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/mobfobamp/folded_lying.jpg"&#10; alt="Image: amp folded, flat. Enclosure is less than 10cm strong, the speaker back protrudes by 8cm."&gt;&lt;figcaption&gt;Consumes very little space when folded&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-1"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/mobfobamp/folded_standing.jpg"&#10; alt="Image: amp folded, standing by itself."&gt;&lt;figcaption&gt;Easy to carry, weight is less than 8kg.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-2"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/mobfobamp/unfolded_side.jpg"&#10; alt="Image: amp unfolded, side view, the speaker chassis protrudes from the speaker face."&gt;&lt;figcaption&gt;28l of volume when unfolded.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-3"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/mobfobamp/unfolded_top.jpg"&#10; alt="Image: amp unfolded, top view. An 18mm thick beech panel keeps the enclosure shut and is sturdy enough to be sat upon."&gt;&lt;figcaption&gt;Height of 20 inches, can be used as a stool.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-4"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/mobfobamp/unfolded.jpg"&#10; alt="Image: amp unfolded, front view. Battery and amplifier board are visible at the combo&amp;#39;s front."&gt;&lt;figcaption&gt;6.5 inch low-end driver, 4Ah battery pack for hours of battery life.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-5"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/mobfobamp/inside_spacer.jpg"&#10; alt="Image: view inside the amp: Nothing but air and a strengthening panel."&gt;&lt;figcaption&gt;A peek inside&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&#10;&lt;/section&gt;&#10;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--center media-frame--audio"&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 Sound sample&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--audio"&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 comparison Sealed vs. Vented&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--audio"&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 and Piano, Sealed, Preamped, at maximum volume&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;All of these tracks have been recorded with my Smartphone, so no high-end gear used. Still, I think they represent the actual sound of MobFobAmp quite well.&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;To overcome many of the constraints of my &lt;a href="https://blog.schallbert.de/en/projects/carsubamp/"&gt;previous prototype&lt;/a&gt;, I decided I&amp;rsquo;d need a bigger speaker, and a more voluminous enclosure to yield a better response on lower frequencies. Unfortunately, a bigger enclosure would considerably reduce mobility, so maybe I could get rid of all the air inside the box for carrying&amp;hellip;?&lt;/p&gt;&#10;&lt;h2 id="the-idea-folding-box"&gt;The Idea: Folding box&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://thumb.wikimedia.org/wikipedia/commons/thumb/0/0c/Faltbehaelter01.jpg/330px-Faltbehaelter01.jpg" alt="folding box"&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;A folding box&lt;/span&gt;&lt;a&#10; href="https://commons.wikimedia.org/wiki/File:Faltbehaelter01.jpg"&#10; class="attr-link"&#10; aria-label="Attribution 1"&#10; &gt;&#10; &lt;sup class="attr-id"&gt;[1]&lt;/sup&gt;&#10; &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;Following the concept of a folding box like shown, I decided my combo should be collapsible for easy carrying. I chose a sealed design for starters so I wouldn&amp;rsquo;t have to care about calculating vents, resonance frequencies and group delays.&lt;/p&gt;&#10;&lt;p&gt;I had to think hard how to integrate speakers and electronics while keeping the ability to fully fold the cabinet.&lt;/p&gt;&#10;&lt;h3 id="dimensions"&gt;Dimensions&lt;/h3&gt;&#10;&lt;p&gt;I selected the cabinet&amp;rsquo;s &lt;code&gt;width&lt;/code&gt; in a way that it would fit snugly into my bass gig-bag if needed. All other dimensions fell into place by itself - &lt;code&gt;height&lt;/code&gt; had to be at least twice the width so top and bottom lids could be accomodated when folded (but not too high to keep stiffness as high as possible. &lt;code&gt;Depth&lt;/code&gt; had to be less than &lt;code&gt;(0.5* width) - (materialStrength + hingeThickness)&lt;/code&gt;, otherwise folding wouldn&amp;rsquo;t work as the sides would get in their way.&lt;/p&gt;&#10;&lt;h3 id="sound-hardware"&gt;Sound Hardware&lt;/h3&gt;&#10;&lt;p&gt;For the power Amplifier I chose to stick with the module I already bought earlier.&#10;I chose a &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.5inch Hi-Fi woofer (50W, 4Ohm)&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; with low resonance frequency to match my sealed cabinet, which I figured would have a volume of &lt;code&gt;~28l&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="simulation"&gt;Simulation&lt;/h3&gt;&#10;&lt;p&gt;Then I ran some &lt;a href="https://lautsprechershop.de/tools/t_box_closed.htm" target="_blank" rel="noopener noreferrer" class="external-link"&gt;simulations&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; to see whether cabinet design and speaker would be a good match.&lt;/p&gt;&#10;&lt;p&gt;Estimated Qtc would be at &lt;code&gt;0.63&lt;/code&gt; instead of the recommended &lt;code&gt;0.71&lt;/code&gt; (optimal step response), so the cabinet would be somewhat overdamped and sound &amp;ldquo;tighter&amp;rdquo; but loose volume at the low end compared to an optimal setting. As you can see, building a cabinet always is a compromise between sound, power, size, weight, frequency response, and volume level that can be obtained from that setting.&lt;/p&gt;&#10;&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;&#10;&lt;p&gt;When I finished building that cabinet after some &lt;code&gt;15h&lt;/code&gt;, I was quite happy to see the cabinet is airtight and not flimsy at all. Unfolding would take me below &lt;code&gt;60 seconds&lt;/code&gt;, folding it back to its original state even less than &lt;code&gt;30 seconds&lt;/code&gt;. As maximum level still was a bit low, I added a compressor stomp pedal to the setup which had a gain knob I could use as a preamp.&lt;/p&gt;&#10;&lt;p&gt;The sound of this thing is amazing. Tight Bass, warm mids and subtle, but brilliant highs fitting the characterisitc sound of the bass guitar very well. Maximum sound level is limited, but it&amp;rsquo;s always enough to keep up with an acoustic guitar. I&amp;rsquo;m using it as a practising amp all the time.&lt;/p&gt;&#10;&lt;h2 id="sound-quality"&gt;Sound quality&lt;/h2&gt;&#10;&lt;p&gt;For sure, the most important aspect of an instrument combo is how it sounds like. Due to the foldable design, it can be operated in two different setups.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Sealed: Tight and well-balanced sound, good low-end punch. Without preamp, maximum level OK to play along with an Acoustic Guitar&lt;/li&gt;&#10;&lt;li&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/bottom_vented.jpg" alt="Vented setup, lid fixed open"&gt;&lt;/figure&gt;Vented: Bottom Lid opened &lt;code&gt;~3cm&lt;/code&gt;, latch suspending cabinet against bottom panel. Much louder sound with pronounced low-mids, less balanced and a bit boomy, maximum level on par with Piano played &lt;em&gt;f&lt;/em&gt;.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I&amp;rsquo;m really happy with the sound quality MobFobAmp provides. The option to play &amp;ldquo;open&amp;rdquo; adds flexibility and makes the sound really stand out (with the drawbacks mentioned above).&lt;/p&gt;&#10;&lt;p&gt;I also have performed some measurements to see how close the design gets to an ideal frequency response, sending some &lt;a href="https://en.wikipedia.org/wiki/White_noise" target="_blank" rel="noopener noreferrer" class="external-link"&gt;White Noise&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; to MobFobAmp and then recording the sound output with a microphone. For comparison, I displayed a spectrum analysis in the frequency domain. A perfectly neutral sound output would hit the orange line at &lt;code&gt;0dB&lt;/code&gt; for the whole spectrum. &lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/frequency_response.jpg" alt="White noise frequency response"&gt;&lt;/figure&gt; You can see, that the &lt;code&gt;-3dB&lt;/code&gt; limits or useable frequency range is from &lt;code&gt;30Hz&lt;/code&gt; to &lt;code&gt;~1.5kHz&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h2 id="add-ons"&gt;Add-Ons&lt;/h2&gt;&#10;&lt;h3 id="9v-power-supply-for-pedal-board"&gt;9V power supply for Pedal Board&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/mobfobamp/7809.jpg" alt="9V linear voltage regulator TO-220"&gt;&lt;/figure&gt;I&amp;rsquo;m using some effects pedals when practising, and I also wanted to be able to use them when I&amp;rsquo;m taking MobFobAmp with me. That&amp;rsquo;s why I decided that it might be a good idea to have a &lt;code&gt;+9V&lt;/code&gt; supply rail with barrel connector (barrel = +) on board.&#10;So I bought a very well-known positive linear voltage regulator from the 78xx series called 7809 in a TO-220 design that can supply up to &lt;code&gt;1.5A&lt;/code&gt; of current at &lt;code&gt;9V&lt;/code&gt;, and accepts an input voltage range of &lt;code&gt;11-35V&lt;/code&gt;. The component can be used with minimal additional parts as shown in the image on the right. This way, I can now drive a pedal board from the battery of MobFobAmp!&lt;/p&gt;&#10;&lt;h3 id="piezo-tweeter"&gt;Piezo Tweeter&lt;/h3&gt;&#10;&lt;p&gt;When I first used a drum computer with mobfobamp, I felt that the highs just were not enough. While perfect for an electric Bass, the drum machine&amp;rsquo;s hi-hat, cymbals, and even the snare sounds dull and mumbly. So I ordered a simple piezo driver which I could connect in parallel to the main speaker - this works perfectly well. I added a &lt;code&gt;2W/320Ohm&lt;/code&gt; resistor in series with the driver to get the levels just right to my ears. The result is great &lt;strong&gt;TODO: and can be heard here&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;h2 id="mobfobamp-pros"&gt;MobFobAmp Pros:&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Foldable to very compact dimensions&lt;/li&gt;&#10;&lt;li&gt;Low Weight: &lt;code&gt;7.3kg&lt;/code&gt; incl. Battery, &lt;code&gt;6.9kg&lt;/code&gt; without&lt;/li&gt;&#10;&lt;li&gt;Runs &lt;code&gt;&amp;gt;20h&lt;/code&gt; on a battery charge as a practising amplifier and moderate volume&lt;/li&gt;&#10;&lt;li&gt;Runs &lt;code&gt;&amp;gt;3h&lt;/code&gt; on a battery charge at maximum volume&lt;/li&gt;&#10;&lt;li&gt;Sounds really good for Bass, Organ or Acoustic Guitar&lt;/li&gt;&#10;&lt;li&gt;Playing low notes down to &lt;code&gt;B0&lt;/code&gt; works fine and sounds punchy&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;9V DC&lt;/code&gt; out for preamps or effects boards&lt;/li&gt;&#10;&lt;li&gt;Can be operated in &amp;ldquo;Bass Reflex Mode&amp;rdquo; for more Bass&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="imperfections"&gt;Imperfections:&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Without Piezo tweeter, highs are muffled&lt;/li&gt;&#10;&lt;li&gt;Low power amp headroom: When turning up the external preamp too much, the power amp will distort notes, especially low ones, first with flutter, then with the typical sound of transistor saturation&lt;/li&gt;&#10;&lt;li&gt;Prototype&amp;rsquo;s rat&amp;rsquo;s nest wiring is still prone to mechanical damage, so an additional enclosure for the electronics would be nice&lt;/li&gt;&#10;&lt;li&gt;Of course, not enough volume/ power to match a drumset or plugged band setup.&lt;/li&gt;&#10;&lt;li&gt;Protective grill for speaker would be nice&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;If you like this idea, feel free to build your own copy! You&amp;rsquo;ll learn what it takes below.&#10;&lt;a href="https://blog.schallbert.de/en/mobfobamp-diy/"&gt;Mobfobamp DIY instructions&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 by &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/en/projects/casemodding-htpc/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/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="project-overview"&gt;Project Overview&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Difficulty: Amateur 3/5&lt;/li&gt;&#10;&lt;li&gt;Cost: ~€500&lt;/li&gt;&#10;&lt;li&gt;Time required: ~20 hours&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="goal-and-motivation"&gt;Goal and Motivation&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--center"&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;p&gt;In 2006, I treated myself to a gaming PC. I bought an HTPC case. It&amp;rsquo;s relatively flat and fits in the rack of a stereo system. In 2015, I replaced the components for the first time when the hardware became too slow. By the end of 2025, it was time again: The power supply was struggling with the power consumption of the new graphics card, the hard drives were filling up, and games were no longer running stable.&lt;/p&gt;&#10;&lt;h2 id="upgrades"&gt;Upgrades&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--right"&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;p&gt;In short: New hardware is needed. However, this new hardware generates significantly more heat than the previous system. Therefore, some modifications are necessary to optimize airflow. Sounds like a great project with many facets that define this blog: mechanics, electronics, software, and CNC milling.&lt;/p&gt;&#10;&lt;h3 id="new-components"&gt;New Components&lt;/h3&gt;&#10;&lt;p&gt;To be installed:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;650W Enermax power supply replacing 400W&lt;/li&gt;&#10;&lt;li&gt;New MSI B550 motherboard&lt;/li&gt;&#10;&lt;li&gt;AMD Ryzen 5600X replacing Intel i5-6500&lt;/li&gt;&#10;&lt;li&gt;32GB DDR4 RAM instead of 16GB&lt;/li&gt;&#10;&lt;li&gt;1TB PCIe SSD instead of SATA SSDs&lt;/li&gt;&#10;&lt;li&gt;140mm fan in the 5.25&amp;quot; bay&lt;/li&gt;&#10;&lt;li&gt;2x60mm fans in the rear exhaust vents&lt;/li&gt;&#10;&lt;li&gt;96mm CPU cooler with a new heatpipe cooler&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I&amp;rsquo;m getting most of the components used. Only power supply, hard drive, and fans are new. However, it quickly becomes apparent that I need to modify the case: The graphics card is so long that it collides with the 3.5&amp;quot; bay, where the hard drives and card reader were previously located. The large 140mm fan only fits in the space where the 5.25&amp;quot; drive bay is located.&lt;/p&gt;&#10;&lt;p&gt;Therefore, both bays must be removed or modified with an angle grinder to create space. I also need a mounting bracket for the fan in front of the 5.25&amp;quot; opening. A nice finishing touch for the case would be the ability to automatically control the drive bay door. Then it would only open when the fan is running under heavy load and remain closed during normal operation.&lt;/p&gt;&#10;&lt;h3 id="modifying-the-case-lid"&gt;Modifying the Case Lid&lt;/h3&gt;&#10;&lt;p&gt;In my article on &lt;a href="https://blog.schallbert.de/en/milling-secc/"&gt;milling SECC steel&lt;/a&gt;, I show how to add extra slots to the case lid for better ventilation of the graphics card.&lt;/p&gt;&#10;&lt;h3 id="35-bay"&gt;3.5&amp;quot; Bay&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;The graphics card is overlaps the 3.5&amp;quot; bay by 1cm. Luckily, the bay can be removed in no time. So I grab the angle grinder and cut a section out of the bracket. Then I drill a few additional holes in the sides of the bay so that my 2.5&amp;quot; SSDs can be mounted on the side of the bay opposite the graphics card. The bay cover also gets another hole for attaching it to the case, since two locking tabs are no longer available due to the cutout for the graphics card.&lt;/p&gt;&#10;&lt;p&gt;The card reader uses the full depth of the bay. I&amp;rsquo;m already worried that I won&amp;rsquo;t be able to reinstall it at all. I opened its casing as a test, and lo and behold: only the front third is actually occupied by circuit boards. The rear section appears to exist solely for mounting. So I cut off the rear part of the card reader casing and reassemble everything so that it now fits properly in the slot.&lt;/p&gt;&#10;&lt;h3 id="525-bay"&gt;5.25&amp;quot; Bay&lt;/h3&gt;&#10;&lt;p&gt;The bay is tall enough to accommodate two drives. However, I hardly ever use CDs or DVDs anymore — and when I do, an external drive suffices. So, I can remove the empty drive cage and mount a fan in its place. To do this, I&amp;rsquo;m designing an adapter bracket in CAD that includes mounting points and cutouts for the fan.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&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;h3 id="fan-placement"&gt;Fan Placement&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&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;I strategically place case fans to create two separate airflows within the PC case: The CPU power supply airflow is directed to the front of the case via a 140mm fan, while the airflow to the rear of the case is directed to the CPU cooler, from where the warm air is exhausted through the power supply and two additional 60mm case fans.&lt;/p&gt;&#10;&lt;p&gt;Airflow to the graphics card is assisted by a 140mm fan mounted outside the case. The graphics card itself exhausts the heated air directly through the case lid.&lt;/p&gt;&#10;&lt;p&gt;To connect the external fan, I cut a hole in a slot cover for PCI slots. The socket of a fan connector press-fits into this hole. I secure the socket in the cover with hot glue and connect the fan to an AUX fan header.&lt;/p&gt;&#10;&lt;h2 id="front-panel-control"&gt;Front Panel Control&lt;/h2&gt;&#10;&lt;p&gt;Now, the front panel should open automatically as soon as the fan spins. It&amp;rsquo;s more complicated than doing it manually, but this feature really makes the case interesting. So, I remove the panel lock and add a mount for a model servo to the adapter bracket, which will open and close the panel.&lt;/p&gt;&#10;&lt;h3 id="electronics"&gt;Electronics&lt;/h3&gt;&#10;&lt;p&gt;The servo requires a control signal. I also want to set the flap position, like the fan, based on temperature, so I need to read the voltage applied to the fan. For this, I&amp;rsquo;m using the 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;. I&amp;rsquo;ve populated the circuit with only the bare essentials:&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/htpc/servo-control-pcb.avif" alt="Image: servo control circuit board"&gt;&lt;/figure&gt;&#10;The microcontroller outputs a pulse-width modulated signal to the servo. Its power supply is buffered by an electrolytic capacitor. I&amp;rsquo;m routing the power supply for the case fan via two connector strips. There, I tap the signal using a &lt;a href="https://en.wikipedia.org/wiki/Voltage_divider" target="_blank" rel="noopener noreferrer" class="external-link"&gt;voltage divider&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and feed it to an analog input. Otherwise, there&amp;rsquo;s only one additional resistor, which pulls the microcontroller&amp;rsquo;s RESET input to &lt;code&gt;VCC&lt;/code&gt;, keeping it powered on.&lt;/p&gt;&#10;&lt;h3 id="software-requirements"&gt;Software Requirements&lt;/h3&gt;&#10;&lt;p&gt;The software requirements are simple and clear:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;When the fan is off, the flap should close.&lt;/li&gt;&#10;&lt;li&gt;When the fan is running, the flap should open.&lt;/li&gt;&#10;&lt;li&gt;To prevent the flap from moving back and forth too frequently, the flap&amp;rsquo;s state should be changed with a slight delay and only if the fan has maintained its current state in the meantime.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;The resulting software is available on &lt;a href="https://github.com/Schallbert/ServoKlappe/tree/main" target="_blank" rel="noopener noreferrer" class="external-link"&gt;my GitHub account&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="software-main-loop"&gt;Software: Main Loop&lt;/h3&gt;&#10;&lt;p&gt;I keep things simple when controlling the model servo: I use the Arduino &lt;a href="https://docs.arduino.cc/libraries/servo/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Servo library&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;The program doesn&amp;rsquo;t do much. I initialize the servo &lt;code&gt;servoHatch&lt;/code&gt;, which is controlled later in the program, define a few variables, and specify which pin the fan&amp;rsquo;s voltage divider is connected to. The main loop simply reads the voltage at the fan and decides, based on that, whether the flap should move.&lt;/p&gt;&#10;&lt;p&gt;I pass the evaluation to the flap control. The program then waits 15ms and starts again.&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-hysteresis"&gt;Software: Hysteresis&lt;/h3&gt;&#10;&lt;p&gt;Using the &lt;code&gt;mapHatchState&lt;/code&gt; function, I define the target state of the hatch based on the voltage at the fan.&lt;/p&gt;&#10;&lt;p&gt;If the voltage remains below approximately 4.8V for a sufficient duration, the hatch closes or remains closed. If it exceeds this voltage, the hatch should open. The duration of the state change depends on the voltage itself; at values ​​far below the threshold, the aggregator fills up more quickly, and the hatch state also changes more rapidly.&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-moving-the-hatch"&gt;Software: Moving the Hatch&lt;/h3&gt;&#10;&lt;p&gt;The &lt;code&gt;setHatch()&lt;/code&gt; function checks the desired state of the hatch and moves it accordingly if it deviates from its current state.&#10;A target angle is specified to the servo using &lt;code&gt;write()&lt;/code&gt;, which is then set and maintained. The movement is gradual and occurs through iterations of &lt;code&gt;hatchState&lt;/code&gt;, as otherwise the hatch would move far too jerkily and quickly.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&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;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Testing the flap control by adjusting the fan&amp;#39;s voltage&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&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;The &lt;code&gt;attach()&lt;/code&gt; and &lt;code&gt;detach()&lt;/code&gt; functions define the signal output to the servo, internally assigning the microcontroller pin to the servo exactly as it&amp;rsquo;s wired. In my experiments, I discovered that the servo consumes very little power when stationary in the &lt;code&gt;detached&lt;/code&gt; state — much less than when it&amp;rsquo;s left &lt;code&gt;attached&lt;/code&gt; and simply not sending any further movement signals. Therefore, I attach it for each movement and then detach it again afterward.&lt;/p&gt;&#10;&lt;h2 id="result"&gt;Result&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--center"&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;p&gt;I now have a nicely organized, modern PC. Using various bloatware solutions (which I uninstall immediately after finishing the work), I manage, after some cursing, to synchronize all the LED colors. A particular highlight is the automatic flap, which activates along with the fan when the CPU load increases.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&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;</description></item><item><title>Privacy</title><link>https://blog.schallbert.de/en/privacy/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/privacy/</guid><description type="html">&#10;&lt;h2 id="schallberts-view-on-privacy"&gt;Schallbert&amp;rsquo;s view on privacy&lt;/h2&gt;&#10;&lt;p&gt;I don&amp;rsquo;t like being tracked myself, so I&amp;rsquo;m trying to to author a site for you that saves data to the max. This website is static, so it won&amp;rsquo;t try loading dynamic contents or third partie&amp;rsquo;s dynamic plugins except for &lt;a href="https://makertube.net/c/cnc_corner/videos?s=1" target="_blank" rel="noopener noreferrer" class="external-link"&gt;my channel on the video platform MakerTube&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. It provides just as many connections to third parties as are absolutely necessary these days to yield a website with a fairly modern and comfortable-to-navigate design. You won&amp;rsquo;t find the typical cookie banner here because there just are none that I&amp;rsquo;m collecting.&lt;/p&gt;&#10;&lt;h3 id="privacy-summary"&gt;Privacy summary&lt;/h3&gt;&#10;&lt;p&gt;This website uses&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;No cookies&lt;/li&gt;&#10;&lt;li&gt;No tracking / trackers&lt;/li&gt;&#10;&lt;li&gt;No analytics tools&lt;/li&gt;&#10;&lt;li&gt;No login / identification&lt;/li&gt;&#10;&lt;li&gt;No embedded content that belongs to third parties &lt;em&gt;with one exception: my channel on MakerTube&lt;/em&gt;&lt;/li&gt;&#10;&lt;li&gt;All links you find on my page are &amp;ldquo;pure&amp;rdquo; links without embedded code or scripts. As long as you don&amp;rsquo;t click them, third parties will not know.&lt;/li&gt;&#10;&lt;li&gt;I do log failed authentication attempts to my &lt;a href="https://blog.schallbert.de/en/server-protection/"&gt;Server to protect&lt;/a&gt; from brute force attacks. For that reason, I do register IP addresses and time stamps of these failed login attempts and buffer them in rolling logs. They are then used by another piece of software that modifies IPtables on the server&amp;rsquo;s network layer to effectively ban IPs for a certain time. The gathered rolling logs are deleted automatically every week.&lt;/li&gt;&#10;&lt;li&gt;The hoster of this website is Hetzner. I rent a &amp;ldquo;cloud server&amp;rdquo; to run my services on their virtualized hardware. Hetzner, as the service provider, collects some data when you visit my site. Please view their &lt;a href="https://www.hetzner.com/legal/privacy-policy/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;privacy statement&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; for more information.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="questions-or-concerns"&gt;Questions or concerns?&lt;/h3&gt;&#10;&lt;p&gt;Please contact me via Github Discussions: &lt;a href="https://github.com/Schallbert/schallbert.github.io/discussions" target="_blank" rel="noopener noreferrer" class="external-link"&gt;github.com/Schallbert&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; or using the information on my &lt;a href="https://blog.schallbert.de/en/about/"&gt;About&lt;/a&gt; page.&lt;/p&gt;&#10;&lt;h2 id="datenschutzerklärung"&gt;Datenschutzerklärung&lt;/h2&gt;&#10;&lt;p&gt;🇩🇪 only&lt;/p&gt;&#10;&lt;h4 id="1-datenschutz-auf-einen-blick"&gt;1. Datenschutz auf einen Blick&lt;/h4&gt;&#10;&lt;h5 id="allgemeine-hinweise"&gt;Allgemeine Hinweise&lt;/h5&gt;&#10;&lt;p&gt;Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren personenbezogenen Daten&#10;passiert, wenn Sie diese Website besuchen. Personenbezogene Daten sind alle Daten, mit denen Sie&#10;persönlich identifiziert werden können. Ausführliche Informationen zum Thema Datenschutz entnehmen&#10;Sie unserer unter diesem Text aufgeführten Datenschutzerklärung.&lt;/p&gt;&#10;&lt;h4 id="datenerfassung-auf-dieser-website"&gt;Datenerfassung auf dieser Website&lt;/h4&gt;&#10;&lt;h5 id="wer-ist-verantwortlich-für-die-datenerfassung-auf-dieser-website"&gt;Wer ist verantwortlich für die Datenerfassung auf dieser Website?&lt;/h5&gt;&#10;&lt;p&gt;Die Datenverarbeitung auf dieser Website erfolgt durch den Websitebetreiber. Dessen Kontaktdaten&#10;können Sie dem Abschnitt „Hinweis zur Verantwortlichen Stelle“ in dieser Datenschutzerklärung&#10;entnehmen.&lt;/p&gt;&#10;&lt;h5 id="wie-erfassen-wir-ihre-daten"&gt;Wie erfassen wir Ihre Daten?&lt;/h5&gt;&#10;&lt;p&gt;Ihre Daten werden zum einen dadurch erhoben, dass Sie uns diese mitteilen. Hierbei kann es sich z. B. um&#10;Daten handeln, die Sie in ein Kontaktformular eingeben.&#10;Andere Daten werden automatisch oder nach Ihrer Einwilligung beim Besuch der Website durch unsere IT-Systeme erfasst. Das sind vor allem technische Daten (z. B. Internetbrowser, Betriebssystem oder Uhrzeit&#10;des Seitenaufrufs). Die Erfassung dieser Daten erfolgt automatisch, sobald Sie diese Website betreten.&lt;/p&gt;&#10;&lt;h5 id="wofür-nutzen-wir-ihre-daten"&gt;Wofür nutzen wir Ihre Daten?&lt;/h5&gt;&#10;&lt;p&gt;Ein Teil der Daten wird erhoben, um eine fehlerfreie Bereitstellung der Website zu gewährleisten. Andere&#10;Daten können zur Analyse Ihres Nutzerverhaltens verwendet werden.&lt;/p&gt;&#10;&lt;h5 id="welche-rechte-haben-sie-bezüglich-ihrer-daten"&gt;Welche Rechte haben Sie bezüglich Ihrer Daten?&lt;/h5&gt;&#10;&lt;p&gt;Sie haben jederzeit das Recht, unentgeltlich Auskunft über Herkunft, Empfänger und Zweck Ihrer&#10;gespeicherten personenbezogenen Daten zu erhalten. Sie haben außerdem ein Recht, die Berichtigung oder&#10;Löschung dieser Daten zu verlangen. Wenn Sie eine Einwilligung zur Datenverarbeitung erteilt haben,&#10;können Sie diese Einwilligung jederzeit für die Zukunft widerrufen. Außerdem haben Sie das Recht, unter&#10;bestimmten Umständen die Einschränkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.&#10;Des Weiteren steht Ihnen ein Beschwerderecht bei der zuständigen Aufsichtsbehörde zu.&#10;Hierzu sowie zu weiteren Fragen zum Thema Datenschutz können Sie sich jederzeit an uns wenden.&lt;/p&gt;&#10;&lt;h4 id="2-hosting"&gt;2. Hosting&lt;/h4&gt;&#10;&lt;h5 id="externes-hosting"&gt;Externes Hosting&lt;/h5&gt;&#10;&lt;p&gt;Diese Website wird bei einem externen Dienstleister gehostet (Hoster). Die personenbezogenen Daten, die&#10;auf dieser Website erfasst werden, werden auf den Servern des Hosters gespeichert. Hierbei kann es sich v.&#10;a. um IP-Adressen, Kontaktanfragen, Meta- und Kommunikationsdaten, Vertragsdaten, Kontaktdaten,&#10;Namen, Websitezugriffe und sonstige Daten, die über eine Website generiert werden, handeln.&#10;Der Einsatz des Hosters erfolgt zum Zwecke der Vertragserfüllung gegenüber unseren potenziellen und&#10;bestehenden Kunden (Art. 6 Abs. 1 lit. b DSGVO) und im Interesse einer sicheren, schnellen und effizienten&#10;Bereitstellung unseres Online-Angebots durch einen professionellen Anbieter (Art. 6 Abs. 1 lit. f DSGVO).&#10;Sofern eine entsprechende Einwilligung abgefragt wurde, erfolgt die Verarbeitung ausschließlich auf&#10;Grundlage von Art. 6 Abs. 1 lit. a DSGVO und § 25 Abs. 1 TTDSG, soweit die Einwilligung die Speicherung&#10;von Cookies oder den Zugriff auf Informationen im Endgerät des Nutzers (z. B. Device-Fingerprinting) im&#10;Sinne des TTDSG umfasst. Die Einwilligung ist jederzeit widerrufbar.&#10;Unser Hoster wird Ihre Daten nur insoweit verarbeiten, wie dies zur Erfüllung seiner Leistungspflichten&#10;erforderlich ist und unsere Weisungen in Bezug auf diese Daten befolgen.&lt;/p&gt;&#10;&lt;p&gt;Wir setzen folgenden Hoster 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:#ae81ff"&gt;Hetzner Online GmbH&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;Industriestr. 25&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;91710&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;Gunzenhausen&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;Germany&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;Tel.&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;+49 (0)9831 505-0&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;Fax&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;+49 (0)9831 505-3&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;Email&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;info@hetzner.com&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="3-allgemeine-hinweise-und-pflichtinformationen"&gt;3. Allgemeine Hinweise und Pflichtinformationen&lt;/h4&gt;&#10;&lt;h5 id="datenschutz"&gt;Datenschutz&lt;/h5&gt;&#10;&lt;p&gt;Die Betreiber dieser Seiten nehmen den Schutz Ihrer persönlichen Daten sehr ernst. Wir behandeln Ihre&#10;personenbezogenen Daten vertraulich und entsprechend den gesetzlichen Datenschutzvorschriften sowie&#10;dieser Datenschutzerklärung.&#10;Wenn Sie diese Website benutzen, werden verschiedene personenbezogene Daten erhoben.&#10;Personenbezogene Daten sind Daten, mit denen Sie persönlich identifiziert werden können. Die vorliegende&#10;Datenschutzerklärung erläutert, welche Daten wir erheben und wofür wir sie nutzen. Sie erläutert auch, wie&#10;und zu welchem Zweck das geschieht.&#10;Wir weisen darauf hin, dass die Datenübertragung im Internet (z. B. bei der Kommunikation per E-Mail)&#10;Sicherheitslücken aufweisen kann. Ein lückenloser Schutz der Daten vor dem Zugriff durch Dritte ist nicht&#10;möglich.&lt;/p&gt;&#10;&lt;h5 id="hinweis-zur-verantwortlichen-stelle"&gt;Hinweis zur verantwortlichen Stelle&lt;/h5&gt;&#10;&lt;p&gt;Die verantwortliche Stelle für die Datenverarbeitung auf dieser Website ist:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/address.jpg" alt="Image: summonable address"&gt;&lt;/figure&gt;&#10;&lt;p&gt;Verantwortliche Stelle ist die natürliche oder juristische Person, die allein oder gemeinsam mit anderen über&#10;die Zwecke und Mittel der Verarbeitung von personenbezogenen Daten (z. B. Namen, E-Mail-Adressen o. Ä.)&#10;entscheidet.&lt;/p&gt;&#10;&lt;h5 id="speicherdauer"&gt;Speicherdauer&lt;/h5&gt;&#10;&lt;p&gt;Soweit innerhalb dieser Datenschutzerklärung keine speziellere Speicherdauer genannt wurde, verbleiben&#10;Ihre personenbezogenen Daten bei uns, bis der Zweck für die Datenverarbeitung entfällt. Wenn Sie ein&#10;berechtigtes Löschersuchen geltend machen oder eine Einwilligung zur Datenverarbeitung widerrufen,&#10;werden Ihre Daten gelöscht, sofern wir keine anderen rechtlich zulässigen Gründe für die Speicherung Ihrer&#10;personenbezogenen Daten haben (z. B. steuer- oder handelsrechtliche Aufbewahrungsfristen); im&#10;letztgenannten Fall erfolgt die Löschung nach Fortfall dieser Gründe.&lt;/p&gt;&#10;&lt;h5 id="allgemeine-hinweise-zu-den-rechtsgrundlagen-der-datenverarbeitung-auf-dieser-website"&gt;Allgemeine Hinweise zu den Rechtsgrundlagen der Datenverarbeitung auf dieser Website&lt;/h5&gt;&#10;&lt;p&gt;Sofern Sie in die Datenverarbeitung eingewilligt haben, verarbeiten wir Ihre personenbezogenen Daten auf&#10;Grundlage von Art. 6 Abs. 1 lit. a DSGVO bzw. Art. 9 Abs. 2 lit. a DSGVO, sofern besondere Datenkategorien&#10;nach Art. 9 Abs. 1 DSGVO verarbeitet werden. Sofern Sie in die Speicherung von Cookies oder in den Zugriff&#10;auf Informationen in Ihr Endgerät (z. B. via Device-Fingerprinting) eingewilligt haben, erfolgt die&#10;Datenverarbeitung zusätzlich auf Grundlage von § 25 Abs. 1 TTDSG. Die Einwilligung ist jederzeit&#10;widerrufbar. Sind Ihre Daten zur Vertragserfüllung oder zur Durchführung vorvertraglicher Maßnahmen&#10;erforderlich, verarbeiten wir Ihre Daten auf Grundlage des Art. 6 Abs. 1 lit. b DSGVO. Des Weiteren&#10;verarbeiten wir Ihre Daten, sofern diese zur Erfüllung einer rechtlichen Verpflichtung erforderlich sind auf&#10;Grundlage von Art. 6 Abs. 1 lit. c DSGVO. Die Datenverarbeitung kann ferner auf Grundlage unseres&#10;berechtigten Interesses nach Art. 6 Abs. 1 lit. f DSGVO erfolgen. Über die jeweils im Einzelfall einschlägigen&#10;Rechtsgrundlagen wird in den folgenden Absätzen dieser Datenschutzerklärung informiert.&lt;/p&gt;&#10;&lt;h5 id="hinweis-zur-datenweitergabe-in-die-usa-und-sonstige-drittstaaten"&gt;Hinweis zur Datenweitergabe in die USA und sonstige Drittstaaten&lt;/h5&gt;&#10;&lt;p&gt;Wir verwenden unter anderem Tools von Unternehmen mit Sitz in den USA oder sonstigen&#10;datenschutzrechtlich nicht sicheren Drittstaaten. Wenn diese Tools aktiv sind, können Ihre&#10;personenbezogene Daten in diese Drittstaaten übertragen und dort verarbeitet werden. Wir weisen darauf&#10;hin, dass in diesen Ländern kein mit der EU vergleichbares Datenschutzniveau garantiert werden kann.&#10;Beispielsweise sind US-Unternehmen dazu verpflichtet, personenbezogene Daten an Sicherheitsbehörden&#10;herauszugeben, ohne dass Sie als Betroffener hiergegen gerichtlich vorgehen könnten. Es kann daher nicht&#10;ausgeschlossen werden, dass US-Behörden (z. B. Geheimdienste) Ihre auf US-Servern befindlichen Daten zu&#10;Überwachungszwecken verarbeiten, auswerten und dauerhaft speichern. Wir haben auf diese Verarbeitungstätigkeiten keinen Einfluss.&lt;/p&gt;&#10;&lt;h5 id="widerruf-ihrer-einwilligung-zur-datenverarbeitung"&gt;Widerruf Ihrer Einwilligung zur Datenverarbeitung&lt;/h5&gt;&#10;&lt;p&gt;Viele Datenverarbeitungsvorgänge sind nur mit Ihrer ausdrücklichen Einwilligung möglich. Sie können eine&#10;bereits erteilte Einwilligung jederzeit widerrufen. Die Rechtmäßigkeit der bis zum Widerruf erfolgten&#10;Datenverarbeitung bleibt vom Widerruf unberührt.&lt;/p&gt;&#10;&lt;h5 id="widerspruchsrecht-gegen-die-datenerhebung-in-besonderen-fällen-sowie-gegen-direktwerbung-art-21-dsgvo"&gt;Widerspruchsrecht gegen die Datenerhebung in besonderen Fällen sowie gegen Direktwerbung (Art. 21 DSGVO)&lt;/h5&gt;&#10;&lt;p&gt;WENN DIE DATENVERARBEITUNG AUF GRUNDLAGE VON ART. 6 ABS. 1 LIT. E ODER F DSGVO&#10;ERFOLGT, HABEN SIE JEDERZEIT DAS RECHT, AUS GRÜNDEN, DIE SICH AUS IHRER BESONDEREN&#10;SITUATION ERGEBEN, GEGEN DIE VERARBEITUNG IHRER PERSONENBEZOGENEN DATEN&#10;WIDERSPRUCH EINZULEGEN; DIES GILT AUCH FÜR EIN AUF DIESE BESTIMMUNGEN GESTÜTZTES&#10;PROFILING. DIE JEWEILIGE RECHTSGRUNDLAGE, AUF DENEN EINE VERARBEITUNG BERUHT,&#10;ENTNEHMEN SIE DIESER DATENSCHUTZERKLÄRUNG. WENN SIE WIDERSPRUCH EINLEGEN,&#10;WERDEN WIR IHRE BETROFFENEN PERSONENBEZOGENEN DATEN NICHT MEHR VERARBEITEN, ES&#10;SEI DENN, WIR KÖNNEN ZWINGENDE SCHUTZWÜRDIGE GRÜNDE FÜR DIE VERARBEITUNG&#10;NACHWEISEN, DIE IHRE INTERESSEN, RECHTE UND FREIHEITEN ÜBERWIEGEN ODER DIE&#10;VERARBEITUNG DIENT DER GELTENDMACHUNG, AUSÜBUNG ODER VERTEIDIGUNG VON&#10;RECHTSANSPRÜCHEN (WIDERSPRUCH NACH ART. 21 ABS. 1 DSGVO).&#10;WERDEN IHRE PERSONENBEZOGENEN DATEN VERARBEITET, UM DIREKTWERBUNG ZU BETREIBEN,&#10;SO HABEN SIE DAS RECHT, JEDERZEIT WIDERSPRUCH GEGEN DIE VERARBEITUNG SIE&#10;BETREFFENDER PERSONENBEZOGENER DATEN ZUM ZWECKE DERARTIGER WERBUNG&#10;EINZULEGEN; DIES GILT AUCH FÜR DAS PROFILING, SOWEIT ES MIT SOLCHER DIREKTWERBUNG IN&#10;VERBINDUNG STEHT. WENN SIE WIDERSPRECHEN, WERDEN IHRE PERSONENBEZOGENEN DATEN&#10;ANSCHLIESSEND NICHT MEHR ZUM ZWECKE DER DIREKTWERBUNG VERWENDET (WIDERSPRUCH&#10;NACH ART. 21 ABS. 2 DSGVO).&lt;/p&gt;&#10;&lt;h5 id="beschwerderecht-bei-der-zuständigen-aufsichtsbehörde"&gt;Beschwerderecht bei der zuständigen Aufsichtsbehörde&lt;/h5&gt;&#10;&lt;p&gt;Sie haben das Recht, Daten, die wir auf Grundlage Ihrer Einwilligung oder in Erfüllung eines Vertrags&#10;automatisiert verarbeiten, an sich oder an einen Dritten in einem gängigen, maschinenlesbaren Format&#10;aushändigen zu lassen. Sofern Sie die direkte Übertragung der Daten an einen anderen Verantwortlichen&#10;verlangen, erfolgt dies nur, soweit es technisch machbar ist.&lt;/p&gt;&#10;&lt;h5 id="ssl--bzw-tls-verschlüsselung"&gt;SSL- bzw. TLS-Verschlüsselung&lt;/h5&gt;&#10;&lt;p&gt;Diese Seite nutzt aus Sicherheitsgründen und zum Schutz der Übertragung vertraulicher Inhalte, wie zum&#10;Beispiel Bestellungen oder Anfragen, die Sie an uns als Seitenbetreiber senden, eine SSL- bzw. TLS-Verschlüsselung. Eine verschlüsselte Verbindung erkennen Sie daran, dass die Adresszeile des Browsers von&#10;„http://“ auf „https://“ wechselt und an dem Schloss-Symbol in Ihrer Browserzeile.&#10;Wenn die SSL- bzw. TLS-Verschlüsselung aktiviert ist, können die Daten, die Sie an uns übermitteln, nicht&#10;von Dritten mitgelesen werden.&lt;/p&gt;&#10;&lt;h5 id="auskunft-löschung-und-berichtigung"&gt;Auskunft, Löschung und Berichtigung&lt;/h5&gt;&#10;&lt;p&gt;Sie haben im Rahmen der geltenden gesetzlichen Bestimmungen jederzeit das Recht auf unentgeltliche&#10;Auskunft über Ihre gespeicherten personenbezogenen Daten, deren Herkunft und Empfänger und den&#10;Zweck der Datenverarbeitung und ggf. ein Recht auf Berichtigung oder Löschung dieser Daten. Hierzu sowie&#10;zu weiteren Fragen zum Thema personenbezogene Daten können Sie sich jederzeit an uns wenden.&lt;/p&gt;&#10;&lt;h5 id="recht-auf-einschränkung-der-verarbeitung"&gt;Recht auf Einschränkung der Verarbeitung&lt;/h5&gt;&#10;&lt;p&gt;Sie haben das Recht, die Einschränkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.&#10;Hierzu können Sie sich jederzeit an uns wenden. Das Recht auf Einschränkung der Verarbeitung besteht in&#10;folgenden Fällen:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Wenn Sie die Richtigkeit Ihrer bei uns gespeicherten personenbezogenen Daten bestreiten, benötigen wir&#10;in der Regel Zeit, um dies zu überprüfen. Für die Dauer der Prüfung haben Sie das Recht, die&#10;Einschränkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.&lt;/li&gt;&#10;&lt;li&gt;Wenn die Verarbeitung Ihrer personenbezogenen Daten unrechtmäßig geschah/geschieht, können Sie&#10;statt der Löschung die Einschränkung der Datenverarbeitung verlangen.&lt;/li&gt;&#10;&lt;li&gt;Wenn wir Ihre personenbezogenen Daten nicht mehr benötigen, Sie sie jedoch zur Ausübung,&#10;Verteidigung oder Geltendmachung von Rechtsansprüchen benötigen, haben Sie das Recht, statt der&#10;Löschung die Einschränkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.&lt;/li&gt;&#10;&lt;li&gt;Wenn Sie einen Widerspruch nach Art. 21 Abs. 1 DSGVO eingelegt haben, muss eine Abwägung zwischen&#10;Ihren und unseren Interessen vorgenommen werden. Solange noch nicht feststeht, wessen Interessen&#10;überwiegen, haben Sie das Recht, die Einschränkung der Verarbeitung Ihrer personenbezogenen Daten&#10;zu verlangen.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Wenn Sie die Verarbeitung Ihrer personenbezogenen Daten eingeschränkt haben, dürfen diese Daten – von&#10;ihrer Speicherung abgesehen – nur mit Ihrer Einwilligung oder zur Geltendmachung, Ausübung oder&#10;Verteidigung von Rechtsansprüchen oder zum Schutz der Rechte einer anderen natürlichen oder&#10;juristischen Person oder aus Gründen eines wichtigen öffentlichen Interesses der Europäischen Union oder&#10;eines Mitgliedstaats verarbeitet werden.&lt;/p&gt;&#10;&lt;h4 id="4-datenerfassung-auf-dieser-website"&gt;4. Datenerfassung auf dieser Website&lt;/h4&gt;&#10;&lt;h5 id="server-log-dateien"&gt;Server-Log-Dateien&lt;/h5&gt;&#10;&lt;p&gt;Der Provider der Seiten erhebt und speichert automatisch Informationen in so genannten Server-Log-Dateien, die Ihr Browser automatisch an uns übermittelt. Dies sind:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Browsertyp und Browserversion&lt;/li&gt;&#10;&lt;li&gt;verwendetes Betriebssystem&lt;/li&gt;&#10;&lt;li&gt;Referrer URL&lt;/li&gt;&#10;&lt;li&gt;Hostname des zugreifenden Rechners&lt;/li&gt;&#10;&lt;li&gt;Uhrzeit der Serveranfrage&lt;/li&gt;&#10;&lt;li&gt;IP-Adresse&#10;Eine Zusammenführung dieser Daten mit anderen Datenquellen wird nicht vorgenommen.&#10;Die Erfassung dieser Daten erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO. Der Websitebetreiber hat&#10;ein berechtigtes Interesse an der technisch fehlerfreien Darstellung und der Optimierung seiner Website –&#10;hierzu müssen die Server-Log-Files erfasst werden.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h5 id="anfrage-per-e-mail-oder-telefon"&gt;Anfrage per E-Mail oder Telefon&lt;/h5&gt;&#10;&lt;p&gt;Wenn Sie uns per E-Mail oder Telefon kontaktieren, wird Ihre Anfrage inklusive aller daraus&#10;hervorgehenden personenbezogenen Daten (Name, Anfrage) zum Zwecke der Bearbeitung Ihres Anliegens&#10;bei uns gespeichert und verarbeitet. Diese Daten geben wir nicht ohne Ihre Einwilligung weiter.&#10;Die Verarbeitung dieser Daten erfolgt auf Grundlage von Art. 6 Abs. 1 lit. b DSGVO, sofern Ihre Anfrage mit&#10;der Erfüllung eines Vertrags zusammenhängt oder zur Durchführung vorvertraglicher Maßnahmen&#10;erforderlich ist. In allen übrigen Fällen beruht die Verarbeitung auf unserem berechtigten Interesse an der&#10;effektiven Bearbeitung der an uns gerichteten Anfragen (Art. 6 Abs. 1 lit. f DSGVO) oder auf Ihrer&#10;Einwilligung (Art. 6 Abs. 1 lit. a DSGVO) sofern diese abgefragt wurde.&#10;Die von Ihnen an uns per Kontaktanfragen übersandten Daten verbleiben bei uns, bis Sie uns zur Löschung&#10;auffordern, Ihre Einwilligung zur Speicherung widerrufen oder der Zweck für die Datenspeicherung entfällt&#10;(z. B. nach abgeschlossener Bearbeitung Ihres Anliegens). Zwingende gesetzliche Bestimmungen, insbesondere gesetzliche Aufbewahrungsfristen, bleiben unberührt.&lt;/p&gt;&#10;&lt;h4 id="quelle"&gt;Quelle:&lt;/h4&gt;&#10;&lt;p&gt;&lt;a href="https://www.e-recht24.de" target="_blank" rel="noopener noreferrer" class="external-link"&gt;e-recht24.de&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;</description></item><item><title>QR-Codengrave</title><link>https://blog.schallbert.de/en/projects/qr-codengrave/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/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="project-stats"&gt;Project stats&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Difficulty: hard 4/5&lt;/li&gt;&#10;&lt;li&gt;Cost: 0€&lt;/li&gt;&#10;&lt;li&gt;Time: ~50h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="the-idea"&gt;The idea&lt;/h2&gt;&#10;&lt;p&gt;A friend asked me to fabricate a coaster for him. A custom logo, some letters. Nothing too interesting. On the backside, he wanted to have a &lt;a href="https://en.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; engraved that keeps a link to his website.&lt;/p&gt;&#10;&lt;p&gt;So I had a QR-code created with a &lt;a href="https://www.nayuki.io/page/qr-code-generator-library" target="_blank" rel="noopener noreferrer" class="external-link"&gt;standard web-tool&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and exported as &lt;code&gt;.svg&lt;/code&gt;. Unfortunately, in my CAM I&amp;rsquo;d have to klick every single pixel and create a carve action for that pixel, which would take quite some minutes of my time for click-select-and-enter-value procedures. Plus, I&amp;rsquo;d have to use a tapered tool and V-carve to get a square pixel which takes much more machining time than a round pixel representation.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/qr-codengrave/svg_path_issues.jpg" alt="Image: QR-code SVG in my CAM"&gt;&lt;/figure&gt;&#10;&lt;p&gt;That&amp;rsquo;s where I decided to throw many, many more hours of my time at this problem and come up with a generic solution for QR-code engravings - a piece of software named &lt;strong&gt;QR-codengrave&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;h2 id="getting-started"&gt;Getting started&lt;/h2&gt;&#10;&lt;h3 id="specification"&gt;Specification&lt;/h3&gt;&#10;&lt;p&gt;This is what I wanted the tool to do:&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;Must-haves:&lt;/em&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Take a text input and convert it to a QR-code. No additional config to be done by the user.&lt;/li&gt;&#10;&lt;li&gt;Provide an option to manage different CNC tools.&lt;/li&gt;&#10;&lt;li&gt;Provide inputs for engrave parameters: engrave depth, in-job rapid movement hover overs, flyover height&lt;/li&gt;&#10;&lt;li&gt;Provide inputs for custom workpiece XY0-offsets&lt;/li&gt;&#10;&lt;li&gt;Vectorize the QR-code&amp;rsquo;s pixel output&lt;/li&gt;&#10;&lt;li&gt;Have paths created from the vectors, for simplicity start with an inwards spiral&lt;/li&gt;&#10;&lt;li&gt;Handover the paths to a module that uses the selected tool&amp;rsquo;s parameters to create machine paths&lt;/li&gt;&#10;&lt;li&gt;Have the machine paths converted to G-code that can later be interpreted by my machine&lt;/li&gt;&#10;&lt;li&gt;Thoroughly verify the algorithms with automated unit tests (no crashes in the physical world please)&lt;/li&gt;&#10;&lt;li&gt;Provide a graphical user interface to make parameter input as simple as possible&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;&lt;em&gt;Optionals&lt;/em&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Have a Continuous Integration pipeline created both for my IDE and Github Actions&lt;/li&gt;&#10;&lt;li&gt;Have a Continuous Deploy pipeline created for my IDE (PyInstaller) and Github Actions (Artifacts, releases)&lt;/li&gt;&#10;&lt;li&gt;Add integration tests for the GUI&lt;/li&gt;&#10;&lt;li&gt;Optimize the path generation algorithm as &amp;ldquo;inwards spiral&amp;rdquo; has a high manufacturing quality and is nice to look at but is not very efficient.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="uncertainities"&gt;Uncertainities&lt;/h3&gt;&#10;&lt;p&gt;At the beginning of this project, I had some open questions and risks that could have limited usability of the program or even make the whole project unfeasible. They all turned out to be wrong (thankfully), but still these were the risks that could have rendered all my work useless:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Cameras won&amp;rsquo;t read round dot-as-pixels QR-codes&lt;/li&gt;&#10;&lt;li&gt;The contrast my engraving materials provide is too low so read results could be erratic or not obtainable&lt;/li&gt;&#10;&lt;li&gt;Machining time could be very high, making the project uneconomic&lt;/li&gt;&#10;&lt;li&gt;Limitations of programming tools or my abilities to code could increase development time to where it&amp;rsquo;s not worth putting more effort into&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;That&amp;rsquo;s where I decided to write &lt;a href="https://blog.schallbert.de/en/projects/qr-codengrave/#unit-tests"&gt;unit tests&lt;/a&gt; along with the code to get quick feedback about whether my code could do what it should. And I limited the program&amp;rsquo;s functionality to one engraving strategy and the GUI to only show a preview of the QR-code, not XY-offsets or other fancy stuff for a start.&lt;/p&gt;&#10;&lt;h3 id="programming-language"&gt;Programming language&lt;/h3&gt;&#10;&lt;p&gt;I took some sheets of paper and noted down the essentials.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/qr-codengrave/sketches.jpg" alt="Image: Software planning on paper"&gt;&lt;/figure&gt;&#10;&lt;p&gt;One module shall take care of creating a QR-code from text input, another one should create vectors and paths to have that QR-code engraved, and a GUI in the middle shall host the text input, a machining preview, and input options for milling tools, engrave parameters, and workpiece offsets.&lt;/p&gt;&#10;&lt;p&gt;I had a couple of programming languages that I &amp;ldquo;speak&amp;rdquo; well enough to choose from:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;C++ with QT framework&lt;/li&gt;&#10;&lt;li&gt;Java&lt;/li&gt;&#10;&lt;li&gt;Python with Tkinter framework&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I decided on the - in my view - most simple solution to have a running prototype as quickly as possible: Python with Tkinter. I know tkinter from an &lt;a href="https://github.com/Schallbert/DerWinterNaht" target="_blank" rel="noopener noreferrer" class="external-link"&gt;earlier project&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; of mine and although it has its caveats and limitations, it&amp;rsquo;s still powerful and quick to setup.&lt;/p&gt;&#10;&lt;h3 id="proof-of-concept"&gt;Proof of concept&lt;/h3&gt;&#10;&lt;p&gt;I did some research and found a pretty good 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 library on the web&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; that&amp;rsquo;s available also in Python. Based on its output, I created a vectorizing algorithm that could interrogate the QR-code library to return values of the QR-code&amp;rsquo;s pixels in a scan line:&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;Then, I had to create vectors from these lines, so that consequent pixels with the same values could be merged to a line that&amp;rsquo;s much quicker to manufacture than multiple dots:&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 bistream&#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;These two methods would now be called by a supervising method that knows a spiral path through the QR-code.&#10;My first GUI just had three elements: An entry field for the text to be converted to a QR-code, a button to run the algorithm, and an output canvas where the algorithm&amp;rsquo;s engrave path would be drawn.&lt;/p&gt;&#10;&lt;p&gt;By the way, I used the &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; module to draw the vectorized QR-code path, a tool that&amp;rsquo;s been written for college kids who take a dip into program lamguages like Python. It is slow but was easy to setup so that was enough for me to get started with. I hard-coded it in to save time, and my loops are more C-ish than pythonic but nevermind.&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;This very simple concept looked promising to me but I had to tweak it a couple of hours because my initial output looked like this:&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&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;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;QR-codengrave GUI test&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;Still, after a while I was able to reliably draw a (21x21 Pixel) QR-code on screen, and use my smartphone&amp;rsquo;s camera to yield the webpage link. 🥳 Yeah!&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&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;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;QR-codengrave vectorizer working&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h3 id="unit-tests"&gt;Unit tests&lt;/h3&gt;&#10;&lt;p&gt;The &amp;ldquo;platform&amp;rdquo; parts - &lt;code&gt;vectorize_qr&lt;/code&gt; and &lt;code&gt;machinify_vector&lt;/code&gt; modules both contain a lot of verifiable calculations and business logic. That&amp;rsquo;s why my tests with Python&amp;rsquo;s standard &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; module are quick and easy to write. This is an example where I verify that the line class I created correctly calculates a length (n+1 style):&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;But it quickly became more complex when I wanted to test whether the output of the QR-Code generator library is handled correctly in my program. For that I had to inject the library into my class under test so I could sneak a mock implementation in that&amp;rsquo;s then interrogated from within the test.&lt;/p&gt;&#10;&lt;p&gt;This pattern of dependency injection is very similar to how it&amp;rsquo;s done in &lt;code&gt;C++&lt;/code&gt; where I&amp;rsquo;m much more proficient (see a project &lt;a href="https://blog.schallbert.de/en/projects/platformio_gtestgmock/#dependency-injection"&gt;here&lt;/a&gt; or another one &lt;a href="https://github.com/Schallbert/Tonuino_alternativeFirmware" target="_blank" rel="noopener noreferrer" class="external-link"&gt;there&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, for example), only that in Python far less boilerplate code is needed to get there. Some internet research helped me write the following test case:&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;I inject the &amp;ldquo;real&amp;rdquo; &lt;code&gt;QrCode&lt;/code&gt; library but with one mock method which is of interest to me, &lt;code&gt;get_module()&lt;/code&gt;. In the test, I&amp;rsquo;m asserting that it is called with the expected parameters from within my class under test.&lt;/p&gt;&#10;&lt;p&gt;With these tests in place, I can be pretty sure that the building blocks of my algorithm work as intended, and that regressions are quick to be found.&lt;/p&gt;&#10;&lt;h2 id="the-long-way-to-minimum-viable-product"&gt;The long way to Minimum Viable Product&lt;/h2&gt;&#10;&lt;p&gt;Boosted by this quick win, I went all-in. I had to design a tool selector before implementing the G-code generator because I&amp;rsquo;d have to have the tool properties available before setting dimensions and tool speed / feed for the QR-code engraving action.&lt;/p&gt;&#10;&lt;p&gt;After some unsuccessful tries in the main window, I decided the tool config window should be a dialog instead (that Tkinter calls &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;) so tool parameters can be entered and sent back to the main GUI once complete. I made the dialog modal with&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 it would always have focus to avoid trigger several instances of this window.&#10;I keep the configured tools in a numbered list so once fed, the application would provide the list so the user could select the required tool for the job from a dropdown menu.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/qr-codengrave/tool_config.jpg" alt="Image: QR-codengrave&amp;#39;s tool configuration dialog"&gt;&lt;/figure&gt;&#10;&lt;p&gt;With this step, I had to implement two things:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Checks for value type and range&lt;/li&gt;&#10;&lt;li&gt;A Persistence module so that entered tools would survice an application restart.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="value-type-and-range-checks"&gt;Value type and range checks&lt;/h3&gt;&#10;&lt;p&gt;This could be accomplished using an event hook within the Entry Box widget like so:&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;And then handing over the entered value to a simple validator that just tries to cast the input value into a float and returns False if that generates an error,&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;Range checks would be made not when something is typed into the fields, but only when the &lt;code&gt;OK&lt;/code&gt; button is pressed to relay the entered data back into the application. Once out of range, a warning Popup (&lt;code&gt;tkinter.messagebox&lt;/code&gt; module) would appear pointing to the possible issue.&lt;/p&gt;&#10;&lt;h3 id="persistence"&gt;Persistence&lt;/h3&gt;&#10;&lt;p&gt;The Persistence module was a bit less easy to implement.&#10;I decided on creating an item what C++ guys would call a &lt;em&gt;static class&lt;/em&gt;, i.e. a class that doesn&amp;rsquo;t have to be instantiated to call one of its methods. This class just has two methods called &lt;code&gt;load&lt;/code&gt; and &lt;code&gt;save&lt;/code&gt; that do what their name suggests using Python&amp;rsquo;s &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; for serialization of objects that I wanted to be persisted. In Python, I used the &lt;code&gt;@classmethod&lt;/code&gt; decorator to indicate the static nature of that class.&lt;/p&gt;&#10;&lt;p&gt;Have a look at how simple data serialization can become with the right tools:&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;When this reliably worked, I added an engrave parameter configuration widget.&lt;/p&gt;&#10;&lt;h3 id="engrave-parameters"&gt;Engrave parameters&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/qr-codengrave/engrave_params.jpg" alt="Image: QR-codengrave&amp;#39;s engrave parameter configuration dialog"&gt;&lt;/figure&gt;&#10;To generate G-code from my QR-code data, I not only needed tool and speed / feed data, but also I&amp;rsquo;d have to know how deep to engrave, how far above the workpiece&amp;rsquo;s surface the tool should hover for rapid moves, and what safety height to use for the CNC to return to origin or home.&lt;/p&gt;&#10;&lt;p&gt;It is created in a similar manner to the &lt;code&gt;tool_configuration&lt;/code&gt; dialog and uses the same mechanisms for type and range checking. The only difference is that it&amp;rsquo;s launched not with a button click, but via click on a label that shows the current engrave parameter value in the main window.&lt;/p&gt;&#10;&lt;h3 id="workpiece-xy-zero-offset"&gt;Workpiece XY Zero offset&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/qr-codengrave/xy0_config.jpg" alt="Image: QR-codengrave&amp;#39;s workpiece offset configuration dialog"&gt;&lt;/figure&gt;&#10;The workpiece offset config dialog features some radiobuttons to select most common workpiece origins, and provides a custom option where the user can enter values for workpiece&amp;rsquo;s X and Y axis zero.&#10;A function in the background calculates the required offset relative to the engrave spiral&amp;rsquo;s starting point.&lt;/p&gt;&#10;&lt;p&gt;Normally, I wouldn&amp;rsquo;t want the GUI to do calculations or a lot of branching logic because it&amp;rsquo;s harder to verify as test fixtures become larger and more difficult to maintain. Still, in this case I decided to do the offset calculation with help of a lookup table within the GUI module to avoid having to communicate through all GUI modules to platform and back (4 classes total comms length).&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="initial-release-v10"&gt;Initial release V1.0&lt;/h2&gt;&#10;&lt;p&gt;The initial release is a minimum viable product. It does its job but still has some flaws (like throwing an error when there&amp;rsquo;s no tool selected in the drop-down) and imperfections (like drawing a QR-code is really slow, re-drawing tilts the QR-code a bit etc.) but it should just work, helping the user avoid the worst blunders on their way.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/qr-codengrave/main_application.jpg" alt="Image: QR-codengrave&amp;#39;s main screen"&gt;&lt;/figure&gt;&#10;&lt;p&gt;I have tested the execution on my CNC machine and at least my phone is able to navigate to a website just by holding the camera at the workpiece 📱&lt;/p&gt;&#10;&lt;h2 id="automated-integration-tests-for-the-gui-frustrating-work"&gt;Automated integration tests for the GUI: frustrating work&lt;/h2&gt;&#10;&lt;p&gt;When my initial release was out, I wanted to add some tests related to the GUI. As I didn&amp;rsquo;t manage to have absolutely no business logic there, I at least wanted to verify the range checks as well as the communication with the platform to ensure that it all works as intended.&lt;/p&gt;&#10;&lt;h3 id="stuck-in-messagebox"&gt;Stuck in Messagebox&lt;/h3&gt;&#10;&lt;p&gt;For that, I wrote some tests that ran on my local machine with the disadvantage that when the test hit a warning or error dialog &lt;code&gt;messagebox.showinfo / messagebox.showerror&lt;/code&gt;, a manual user interaction was absolutely required to have the window closed and the test to continue.&lt;/p&gt;&#10;&lt;p&gt;I didn&amp;rsquo;t find an easy way to circumvent that issue as the messagebox acts as an independent widget that cannot easily be tampered with. Even my tries to invoke a keystroke as a side effect from within the test did not prove to be fruitful (and, in the end, wouldn&amp;rsquo;t have been a clean sollution, I suppose).&lt;/p&gt;&#10;&lt;p&gt;My solution after a couple of hours cursing under my breath is that the messagebox widgets are injected into the GUI classes that use it:&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;This way, I can throw &lt;code&gt;mock_msg&lt;/code&gt; into the gui class so not the real showinfo was taken but instead the mock would know whether it has been called as intended. I could add it to the test fixture so my test code itself could stay &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;.&lt;/p&gt;&#10;&lt;p&gt;It adds more arguments to the constructor and delegates the instantiation to a higher level class, but still the usage inside the class under test stays trivial.&lt;/p&gt;&#10;&lt;h3 id="static-class-issues"&gt;Static class issues&lt;/h3&gt;&#10;&lt;p&gt;As the persistence class I mentioned &lt;a href="https://blog.schallbert.de/en/projects/qr-codengrave/#persistence"&gt;above&lt;/a&gt; is &lt;em&gt;static&lt;/em&gt; in a way that it doesn&amp;rsquo;t require instantiation, I had the next issue built in my code right from the start: An integration test would now try to write to an actual &lt;code&gt;persistence.dat&lt;/code&gt; file, a dependency that I didn&amp;rsquo;t want to have, especially not in the continuous integration environment.&lt;/p&gt;&#10;&lt;p&gt;So I moved Persistence up the stream into the main GUI file that wouldn&amp;rsquo;t be tested following &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 Anderson&amp;rsquo;s splendid blog post&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. The main GUI should only contain declarative code anyways, and the loaded data is then cascaded into the classes that use it instead of the persistence class that actually does the load/save operation.&lt;/p&gt;&#10;&lt;h2 id="future-releases"&gt;Future releases&lt;/h2&gt;&#10;&lt;p&gt;This is what I&amp;rsquo;m planning for future releases:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Drawing speed of the QR-code must be increased&lt;/li&gt;&#10;&lt;li&gt;Graphic bugs (scaling, tilting) and blurry edges of drawing screen to be fixed&lt;/li&gt;&#10;&lt;li&gt;CNC paths to be optimized for lower manufacturing time&lt;/li&gt;&#10;&lt;li&gt;Bugfixes&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;At least if there are any; let&amp;rsquo;s see how much I&amp;rsquo;ll be using this tool. The things that annoy me most will likely be fixed first.&lt;/p&gt;&#10;&lt;p&gt;Ah, one last thing: be rewarded for following through on this article. You can download your copy of &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; for free to play around with, and for home use. If you&amp;rsquo;re planning for commercial use, please let me know upfront.&lt;/p&gt;&#10;</description></item><item><title>Seifenbutler</title><link>https://blog.schallbert.de/en/projects/seifenbutler/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/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="project-stats"&gt;Project stats&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Difficulty: Medium 3/5&lt;/li&gt;&#10;&lt;li&gt;Cost: ~250€&lt;/li&gt;&#10;&lt;li&gt;Time: 1+ year&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="the-story-of-seifenbutler"&gt;The story of Seifenbutler&lt;/h2&gt;&#10;&lt;p&gt;Accompany me here over a period of more than a year, in which I am constantly working on one project. It goes from an idea with a few requirements to pre-production readiness. I explain my prototype work, go into a little detail about the many setbacks and finally come closer to what I think is a good product.&lt;/p&gt;&#10;&lt;h2 id="idea"&gt;Idea&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;May 2022&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;I recently switched from using liquid shampoo in the shower to solid shampoo and soap. As I didn&amp;rsquo;t have a tray for the soap, I showered with it. It used up more quickly, didn&amp;rsquo;t dry for days and stuck to the edge of the shower.&lt;/p&gt;&#10;&lt;p&gt;I didn&amp;rsquo;t want to buy a normal soap holder that would require drilling holes in the tiled walls. So I decided to design a clip-on soap holder for the shower rail. I would mount the piece above the shower head so it wouldn&amp;rsquo;t be exposed to more water than necessary.&lt;/p&gt;&#10;&lt;p&gt;The soap holder should also be easy to fit without tools.&lt;/p&gt;&#10;&lt;h2 id="v1-v3-first-drafts-in-hpl"&gt;V1-V3: First drafts in HPL&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;July 2022&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;So I designed my own holder. I wanted a simple, round design with a clamp hole to attach to the shower rail and a sunken area to hold the soap or solid shampoo. A few slots in the base would allow water to drip off and the soap to air dry when not in use. I wanted to add a fastener inside the piece and a nut on the inside to reduce the diameter of the hole for the shower rod and clamp seifenbutler - as the piece is called - in place.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/soapbutler/clamping_explained.jpg" alt="Image: seifenbutler V1&amp;#39;s clamping mechanism explained "&gt;&lt;/figure&gt;&#10;&lt;p&gt;To make this possible, it made sense to produce two components - an upper shell that holds the soap in place and provides space for the mounting mechanism and a lower one that has drainage slots for excess water and holds the other half of the mounting mechanism.&lt;/p&gt;&#10;&lt;h3 id="freecad-sketches"&gt;FreeCAD: Sketches&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/soapbutler/freecad_partbodydwg.jpg" alt="Image: FreeCAD part, body, sketch"&gt;&lt;/figure&gt;&#10;FreeCAD artefacts are organized in a certain way to simplify the handling of different views or to prepare for later two-sided editing using the clone function. Therefore, I first create a series of parts in &amp;lsquo;Parts Design&amp;rsquo; mode. Sketches can be grouped into body elements, which in turn are arranged in part elements.&lt;/p&gt;&#10;&lt;p&gt;So I create a part (yellow element) that contains &amp;ldquo;sketches&amp;rdquo; - all the original drawings (red elements) of all the bodies (blue elements) of the part(s) to be produced. These drawings should be fully constrained. They cannot contain any information on positioning relative to other components. This means that the sketches are all in the same place. Therefore, all inactive sketches should be hidden to avoid confusion.&lt;/p&gt;&#10;&lt;p&gt;(A more in-depth description of my FreeCAD workflow can be found &lt;a href="https://blog.schallbert.de/en/freecad-get-started/"&gt;in this blog post&lt;/a&gt;.)&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/soapbutler/freecad_scetches.jpg" alt="Image: FreeCAD Sketch"&gt;&lt;/figure&gt;&#10;&lt;h3 id="freecad-parts"&gt;FreeCAD: Parts&lt;/h3&gt;&#10;&lt;p&gt;Then I create parts that later contain the components to be cut, e.g. &amp;ldquo;clampingHoles&amp;rdquo;, &amp;ldquo;soapButler&amp;rdquo;, &amp;ldquo;wallHolder&amp;rdquo;. They each contain a subset of sketches, but no information on the final positioning. Finally, I create a part for each job that my production has to perform, e.g. &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;. These clone body components but add placement information, include mirrored parts for 2-sided milling or provide an outline to set the XY-0 correctly.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/soapbutler/freecad_parts.jpg" alt="Image: FreeCAD part"&gt;&lt;/figure&gt;&#10;&lt;p&gt;The advantage of such a project configuration is that if an original sketch has to be changed for any reason, all cloned parts are automatically changed as well. With clever organization, it is also possible to simply change the dimensions of the parts in the table that then adjust the entire part geometry accordingly without help of the user.&lt;/p&gt;&#10;&lt;h3 id="machining"&gt;Machining&lt;/h3&gt;&#10;&lt;p&gt;For my first prototype, I chose high pressure laminate as the material as it is very durable, can get wet from time to time and looks pretty good. For machining, I used low helix endmills to keep the vibration under control when cutting the tough HPL material. I opted for coated single flute carbide bits that I bought some time ago (for cutting aluminum). I used eccentric clamps to hold the part in place and only required two additional hold-downs to keep the part on the machine bed.&lt;/p&gt;&#10;&lt;p&gt;I had an error in the CAM program that took me a few minutes to fix: I had set the toolpaths for the spiral inward instead of outward. The problem with this is that the spiral becomes increasingly spongy and unstable as the cut progresses. I was only able to keep the vibrations in check by frequently interrupting the milling process and applying tape to the areas where the cut had already been made.&lt;/p&gt;&#10;&lt;h3 id="rating"&gt;Rating&lt;/h3&gt;&#10;&lt;figure class="media-frame media-frame--center"&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;p&gt;Production of my first prototype went quite OK. The machine was humming confidently, and apart from a few minor errors (fixing holes in the lower part too narrow, length of clamp fixing incorrectly specified) I was reasonably happy with the result.&lt;/p&gt;&#10;&lt;p&gt;It took me about 20 minutes to release the part and remove the taps. This seemed to be clearly in need of improvement.&lt;/p&gt;&#10;&lt;h2 id="v4-failure-with-cheap-hpl"&gt;V4: Failure with cheap HPL&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;October 2022&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/soapbutler/cam_chamfer_issue.jpg" alt="Image: Chamfer problem with narrow slots"&gt;&lt;/figure&gt;&#10;I&amp;rsquo;ve never used the chamfer function in my CAM before and thought this would be a good opportunity for a test as the part corners were quite sharp on my first prototypes. As I only use a 4mm end mill, the 90° &lt;code&gt;10mm&lt;/code&gt; chamfer cutter seemed too big, especially as I had to offset outwards to avoid wearing out the cutter&amp;rsquo;s tip. I therefore chose a 30° tapered bit with only 6mm diameter, which quickly turned out to be a mistake as it hardly softened the corners.&lt;/p&gt;&#10;&lt;p&gt;I also forgot that only the outside of the parts would benefit from a beveled edge. I, on the other hand, applied the chamfer to the inside, which was detrimental to the finish quality. I also found that the new HPL sheet I used to make the prototypes was not completely flat, so some parts of the outline were not fully milled through. This in turn led to a lower quality and I had to do considerable manual rework.&lt;/p&gt;&#10;&lt;h2 id="v5-corrections-in-cam-and-better-processing-quality"&gt;V5: Corrections in CAM and better processing quality&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;November 2022&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&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;The &amp;ldquo;free arm clamp&amp;rdquo; design with open fastening element is not so nice to look at, I guess, because it is asymmetrical. It would also be difficult to clean because of the narrow gap. After some thought, I came up with the solution of a &amp;ldquo;center clamp&amp;rdquo;: the fastener would press against a piece of rubber that rests directly against the shower rail to hold the soap dish in place.&lt;/p&gt;&#10;&lt;h3 id="chamfers"&gt;Chamfers&lt;/h3&gt;&#10;&lt;p&gt;After updating the drawings accordingly, I created another mirrored part in FreeCAD in &amp;ldquo;Draft&amp;rdquo; mode. This part clones the original part at the outline and is intended for two-sided milling, rotating the part around its top left edge (XY zero). In this way, the chamfer can be implemented correctly if I can ensure that the XY zero point does not move.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&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;p&gt;This worked very well in practice, and I was also able to automate the drilling of countersunk holes for the lower fasteners, which increases the build quality. The result can be seen below.&lt;/p&gt;&#10;&lt;h3 id="rating-1"&gt;Rating&lt;/h3&gt;&#10;&lt;p&gt;Unfortunately, after this fifth piece was finished, I noticed that the melamine coating was slightly chipped at the cut edge. I also noticed the smell of burnt material during processing. A quick look at the cutter revealed the cause: it had become blunt after just one hour of use, despite the coating. This makes working with HPL quite expensive and frustrating. In addition, the cycle time for this version was quite long: 15 minutes for one part. That was never going to be economical&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;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/soapbutler/soapbutler_v5.jpg" alt="Image: Soapbutler_v5 front view with fastener"&gt;&lt;/figure&gt;&#10;&lt;p&gt;That&amp;rsquo;s why I tried to find alternative materials for Seifenbutler.&#10;In the meantime, the prototypes should be tried out in the shower for a few months to gather long-term experience. Is HPL suitable for use in damp areas? Does the water creep through between the two layers? Is it easy to clean?&lt;/p&gt;&#10;&lt;h2 id="v6-failure-with-birch-multiplex"&gt;V6: Failure with birch multiplex&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;May 2023&lt;/strong&gt;&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&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;p&gt;In my search for new materials, I procured waterproof wooden boards, glued together in multiplex method. As it was considerably thicker than the previously used material (&lt;code&gt;15mm&lt;/code&gt;), I simplified the design into a single-piece version with a grub screw at the side to clamp it to the shower rail.&lt;/p&gt;&#10;&lt;p&gt;I would drill the hole for the grub screw by hand after production on the CNC.&lt;/p&gt;&#10;&lt;p&gt;Unfortunately, however, the material on CNC turned out to be unsuitable for milling the spiral. I got numerous tear-outs and had difficulty holding the workpiece down. The contours of my Seifenbutler were simply too fine for the cross-glued material. Maybe the wood&amp;rsquo;s fiber length was too high, too.&lt;/p&gt;&#10;&lt;p&gt;The five machined parts went straight into residual waste.&lt;/p&gt;&#10;&lt;h2 id="v7-failure-with-aluminium-composite-material"&gt;V7: Failure with Aluminium-Composite material&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;Juli 2023&lt;/strong&gt;&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&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;p&gt;With this design, I returned to my two-component setup and wondered whether Aluminium composite would be a suitable material for Seifenbutler. It is known for its durability and is often used outdoors, at least.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--right"&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;p&gt;Without further ado, I drew up some new sketches&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; that tried to take properties of the new material into account. The light material would finally make it possible to use magnets to hold it in place, so I made appropriate countersinks in the frame.&lt;/p&gt;&#10;&lt;p&gt;I had the idea of creating a part from each side of the shower rail and then holding it together using the magnets - this finally got rid of the annoying clamping screw and gave me a very simple design.&lt;/p&gt;&#10;&lt;p&gt;Unfortunately, however, the concept turned out to be completely unsuitable. The holding force of the magnets is not high enough and the clamping only works by tilting the soap holder forwards, which gives a strangely crooked impression on the shower rail.&lt;/p&gt;&#10;&lt;p&gt;So: keep tinkering!&lt;/p&gt;&#10;&lt;h2 id="v8-switch-to-acrylic-glass-chuck"&gt;V8: Switch to acrylic glass, chuck&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;July 2023&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;After many hours at the computer for the design, I came up with a new clamping principle, copied from the drill chuck of my hand drill: three movable clamping devices mounted at a 120° angle - brought together by means of a cable tie - would hold shower rails of any diameter known to me.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&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;h3 id="manufacturing"&gt;Manufacturing&lt;/h3&gt;&#10;&lt;p&gt;I encountered various difficulties in production, mainly caused by per design small components, which also had to have notches to guide the cable ties. In addition, I realized that designing and producing smooth fits without noticeable play was a major challenge for me and that it would take a lot of unsuccessful attempts before I reached my goal.&lt;/p&gt;&#10;&lt;p&gt;However, by using different sized (&lt;code&gt;2mm, 4mm&lt;/code&gt;) milling cutters and optimizing the milling sequence, I was finally able to produce the parts successfully and in good quality. See my &lt;a href="https://blog.schallbert.de/en/milling-small-parts/"&gt;article on milling small parts&lt;/a&gt; for reference here. Only the spiral is still giving me a headache, because even in acrylic glass it would prefer to avoid the milling cutter. As a result, its edges are quite sharp and uneven.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&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;Fortunately, it was easy to assemble - It only needs one screw and some plastic glue.&lt;/p&gt;&#10;&lt;h3 id="test"&gt;Test&lt;/h3&gt;&#10;&lt;p&gt;I had this variant in my shower for a few months as a test. As the screw was only galvanized, it unfortunately started to corrode. Note: use stainless steel.&lt;/p&gt;&#10;&lt;p&gt;The stability of Seifenbutler could still be improved. The cable tie leaves a little play, so that with little effort, inclinations of around 10° to the vertical of the shower rail are possible, causing a lot of creaking between the upper and lower parts. This needs to be improved.&lt;/p&gt;&#10;&lt;p&gt;I would also like to simplify the clamping mechanism again.&lt;/p&gt;&#10;&lt;h2 id="v9-cast-acrylic-glass-removable-soap-tray"&gt;V9: Cast acrylic glass, removable soap tray&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;July 2023&lt;/strong&gt;&#10;&lt;figure class="media-frame media-frame--right"&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;&lt;/p&gt;&#10;&lt;p&gt;This evolution step holds upper and lower shells together at the front with magnets that can be invisibly sunk into the material. There are two inserts in the clamping area at the back, which fit into a suitable recess in Seifenbutler like a drawer and hold it together.&lt;/p&gt;&#10;&lt;p&gt;It took me several prototypes to get the tie guide to the point where the cable tie is simply inserted into the back of Seifenbutler, wraps itself around the shower rail and finally reappears where it is ready to be tied down.&lt;/p&gt;&#10;&lt;p&gt;This is the first design that meets my requirements in terms of holding power and ease of assembly. Unfortunately, the production is complex and the tolerances have to be kept low, as the soap holder either slips out of the clamp too easily or can hardly be fitted because the fit is too tight.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&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;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Seifenbutler zip tie demonstration&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="v10-improved-manufacturing-process"&gt;V10: Improved manufacturing process&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;August 2023&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;With a few optimizations, this version corresponds to V9. I achieved better repeatability in manufacturing and I could be a little more generous with tolerances for fits.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&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;p&gt;With this design, I obtain first feedback from a couple of friends. The unanimous opinion: cable ties look like a makeshift solution so customer acceptance might be low.&lt;/p&gt;&#10;&lt;p&gt;So I head back to the drawing board.&lt;/p&gt;&#10;&lt;h2 id="v11-l-nose-plug-in-system-in-hpl"&gt;V11: &amp;lsquo;L-nose&amp;rsquo; plug-in system in HPL&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;August 2023&lt;/strong&gt;&#10;&lt;figure class="media-frame media-frame--right"&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;&lt;/p&gt;&#10;&lt;p&gt;Another attempt in HPL, this time with multi-tooth cutters. They rasp the material rather than cutting it and are designed for processing abrasive, fiber-rich materials such as CFRP/GRP. However, the high toughness of HPL combined with poor heat dissipation means that the milling cutters become completely blunt after just two prototypes have been produced. So they are not a solution for efficient work in HPL for me either.&lt;/p&gt;&#10;&lt;p&gt;The design is again multi-part and achieves the clamping in its last update via a rear-mounted screw, which is tightened after the front and rear parts have been put together. This makes the soap holder longer overall, which I don&amp;rsquo;t find ideal in terms of design.&lt;/p&gt;&#10;&lt;p&gt;On the other hand, I think I now have a good handle on the manufacturing quality. The cuts are clean and the quality of the edges is also very pleasing.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&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;p&gt;Due to the rapid cutter wear, it is very difficult for me to produce a slight oversize fit for the plug-in connection. It happens all too easily in my attempts that the cutter loses diameter too quickly and that parts cannot be inserted into each other at all or only with brute force.&lt;/p&gt;&#10;&lt;p&gt;Perhaps I can solve this problem by using a different fitting geometry?&lt;/p&gt;&#10;&lt;h2 id="v12-brio-plug-in-system"&gt;V12: &amp;lsquo;Brio&amp;rsquo; plug-in system&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;September 2023&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;Here I use acrylic glass again, this time in the extruded version. This way, thickness tolerances are very small compared to the cast version and my Z-zero points always fit. This is great for applying chamfers, which are finally the same width on every part and give the material a high-quality finish.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&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;p&gt;Now I also have a solution for the spiral: double-sided processing. This increases the workload for me, but I&amp;rsquo;d rather spend more time at the machine than be annoyed about poor quality later on.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&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;My procedure: I mill the spiral almost all the way through and leave only a delicate &lt;code&gt;0.3mm&lt;/code&gt;. I then use the chamfer cutter on the back and cut the spiral from inside to outside. I also secure the underside with adhesive tape so that my vacuum table can hold the spiral securely and vibrations are avoided.&lt;/p&gt;&#10;&lt;p&gt;The design includes a knurled screw so clamping can be done without tools. As the clamping exerts strong tensile forces on the connectors and they therefore have to be positioned in the axis of the shower rod for stability reasons, I also have a slight tilting problem here. This problem increases the smaller the diameter of the shower rail is.&lt;/p&gt;&#10;&lt;p&gt;I was able to avoid tilting with another variant, but then the spigots are no longer covered. This in turn means that shower water can now get in there and leave residues.&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;m almost there. Nevertheless, there may still be something I can improve.&lt;/p&gt;&#10;&lt;h2 id="v13-automatic-clamping-and-zero-series-production"&gt;V13: Automatic clamping and zero series production&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;October 2023&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;In version 13, I try to bundle the advantages of the previous versions V9-V12 while avoiding the disadvantages (tilting, cable ties, assembly with tools, unstable) as much as possible.&lt;/p&gt;&#10;&lt;h3 id="new-clamping-concept"&gt;New clamping concept&lt;/h3&gt;&#10;&lt;p&gt;I am therefore pursuing a new clamping concept. I still want to take most commercially available shower rods into account during installation and therefore need to clamp securely between &lt;code&gt;18mm&lt;/code&gt; and &lt;code&gt;25mm&lt;/code&gt;. From the previous versions, I have also realized that additional pressure must be exerted centrally behind the shower rail to prevent Seifenbutler from tilting.&lt;/p&gt;&#10;&lt;p&gt;I am also trying to realize the locking mechanism around the shower rail by means of a rotary movement. Pivot point is the center of the spiral, which requires three additional screw connections around the circumference of Seifenbutler. With &lt;code&gt;V13.9&lt;/code&gt; I finally achieve the breakthrough.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&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;p&gt;The automatic clamping mechanism is activated by pushing upper and lower shells together so that Seifenbutler adjusts continuously to the diameter of the shower rail. It is locked in place by a knurled screw on the underside, where an inclined plane on the catch hook prevents unintentional opening.&lt;/p&gt;&#10;&lt;h3 id="manufacturing-1"&gt;Manufacturing&lt;/h3&gt;&#10;&lt;p&gt;Unfortunately, this design is very complex. It consists of five components made of acrylic glass, five screws and a knurled screw with spring washer. It takes quite some time to assemble.&lt;/p&gt;&#10;&lt;p&gt;The work on the CNC takes over 10 minutes per part, which is a lot. I also have to use consumables due to the required workpiece hold down.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&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;p&gt;Nevertheless, the manufacturing quality is higher than ever before and Seifenbutler is now child&amp;rsquo;s play to fit without tools and within a short time.&lt;/p&gt;&#10;&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;January 2024&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;Looking through my CAD files, I have planned a total of &lt;code&gt;42&lt;/code&gt; versions of my soap holder so far, divided into &lt;code&gt;13&lt;/code&gt; designs, some of which are very different. &lt;code&gt;21&lt;/code&gt; of these versions have actually made it onto the CNC in the end and &lt;code&gt;5&lt;/code&gt; of them have been produced in larger quantities than 3.&lt;/p&gt;&#10;&lt;p&gt;I have four Seifenbutlers in my shower for endurance testing and they are now used by the whole family.&lt;/p&gt;&#10;&lt;p&gt;I can&amp;rsquo;t really count the number of hours I&amp;rsquo;ve spent on the subject so far - but it&amp;rsquo;s been many. I&amp;rsquo;ve learned a lot about materials and their suitability for different part geometries, absorption of forces, their machinability and the necessary cutting values on my machine. And also that it&amp;rsquo;s worth pursuing things with determination over a long period of time. Not in monetary terms, but definitely for me as a person.&lt;/p&gt;&#10;&lt;p&gt;I don&amp;rsquo;t want to rule out making further optimizations to the soap butcher - but first I want to see whether it is &lt;a href="https://blog.schallbert.de/en/seifenbutler-pilot-run/"&gt;marketable as a niche product&lt;/a&gt; in the near future.&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;I give a deeper dive into the challenges of machining HPL &lt;a href="https://blog.schallbert.de/en/cnc-router-overload/"&gt;in this blog post&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;As you can see from the CAD images from here on, I changed my drawing software (from FreeCAD to &lt;a href="https://cadascam.com/" 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;). Despite some disadvantages, it is easier to use for 2D drawings and I get results faster. One big downer: CADasCAM is not open source 😥.&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>Spindle upgrade</title><link>https://blog.schallbert.de/en/projects/spindle-upgrade/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/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="Spindle upgrade" /&gt;&#10;&lt;h2 id="project-stats"&gt;Project stats&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Difficulty: Expert 5/5&lt;/li&gt;&#10;&lt;li&gt;Cost: ~2700€&lt;/li&gt;&#10;&lt;li&gt;Time: ~20h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;iframe&#10; src="https://makertube.net/videos/embed/bN8MKYr4dmZZd2Tmoga9Ny"&#10; title="Upgrading my CNC&amp;#39;s spindle"&#10; loading="lazy"&#10; allow="autoplay; fullscreen; picture-in-picture"&#10; allowfullscreen&#10; style="width: 100%; height: auto; min-height: 315px; aspect-ratio: 16/9;"&#10; &gt;&lt;/iframe&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Upgrading my CNC&amp;#39;s spindle&lt;/span&gt;&lt;a href="https://makertube.net/videos/embed/bN8MKYr4dmZZd2Tmoga9Ny" class="attr-link" target="_blank" rel="noopener noreferrer"&gt;↗&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;In contrast to my statements &lt;a href="https://blog.schallbert.de/en/portal-milling/#spindle-power"&gt;here&lt;/a&gt; I decided to invest in a High-Frequency spindle. I went through a lot of reading, building and &lt;a href="https://blog.schallbert.de/en/testing-hardware/"&gt;testing&lt;/a&gt; effort before I could finally create chips with the new setup.&lt;/p&gt;&#10;&lt;h2 id="why-i-upgraded"&gt;Why I upgraded&lt;/h2&gt;&#10;&lt;p&gt;There are strong and weak arguments for such an upgrade. Long story short: My arguments were rather weak so I could have sticked to the router motor I already had collected experience with. The decision to upgrade anyways was on the non-rationale side.&lt;/p&gt;&#10;&lt;h3 id="strong-arguments-to-upgrade-"&gt;Strong arguments to upgrade 🥰&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;More power: &lt;a href="https://blog.schallbert.de/en/cnc-router-overload/"&gt;This&lt;/a&gt; is not supposed to happen again, and I can run high feed rates with confidence. As a result, machining time reduces for projects that require removing loads of material.&lt;/li&gt;&#10;&lt;li&gt;More low-end torque: With a face mill cutter, the router motor tended to overheat as the bolt-on fan wouldn&amp;rsquo;t provide enough cooling at low rotation speeds.&lt;/li&gt;&#10;&lt;li&gt;Vibrations: I hope that the &lt;a href="https://blog.schallbert.de/en/cnc-vibrates/"&gt;vibration issue&lt;/a&gt; is gone now because both spindle, its attachment to the Z-axis, and vacuum table provide the CNC with higher stiffness.&lt;/li&gt;&#10;&lt;li&gt;Likely higher precision of cut on the spindle due to higher-grade bearings and more rigid design&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="weak-arguments-to-upgrade-"&gt;Weak arguments to upgrade 🤔&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;I was very interested in variable frequency drive and spindle technology&lt;/li&gt;&#10;&lt;li&gt;I liked the looks of the Spinogy spindle.&lt;/li&gt;&#10;&lt;li&gt;I was in contact with Spinogy for half a year discussing possible configurations and the adaptation to my machine and I didn&amp;rsquo;t want to let them down&lt;/li&gt;&#10;&lt;li&gt;The VFD has internal safety systems that reduce probability of damaged workpieces&lt;/li&gt;&#10;&lt;li&gt;The HF-spindle is more quiet than a router motor&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="arguments-not-to-upgrade-"&gt;Arguments not to upgrade 🤨&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Low value add for money: Versatility of the machine is increased by just a little while upgrade costs are really high&lt;/li&gt;&#10;&lt;li&gt;Spindle warm-up takes &lt;code&gt;30 minutes&lt;/code&gt;, a considerable amount of time when you&amp;rsquo;re not using the machine 24/7&lt;/li&gt;&#10;&lt;li&gt;Steep learning curve required for configuring VFD/Spindle, &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;&amp;gt;400 pages of VFD&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;and spindle manuals&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; to read, circuit layouts to plan, and a switching cabinet to build.&lt;/li&gt;&#10;&lt;li&gt;The CNC frame I have is laid out for light to medium usage. Turning up the feedrates because of the new spindle&amp;rsquo;s capabilities could overburden frame and structure, especially the machine&amp;rsquo;s weak spot, the Z-axis.&lt;/li&gt;&#10;&lt;li&gt;The HF-spindle weighs 3x more compared to the router motor.&lt;/li&gt;&#10;&lt;li&gt;A VFD system makes the whole CNC much more complex and adds a multitude of paths for disturbances.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;All in all the spindle is overpowered for the rest of the machine and the benefit for adding the spindle likely will be low. You see this was an emotional decision. I hope I won&amp;rsquo;t regret it.&lt;/p&gt;&#10;&lt;h2 id="auto-tool-change"&gt;Auto tool change?&lt;/h2&gt;&#10;&lt;p&gt;If I had a well-running woodworking business with clear scope and just a few endmills in use, I&amp;rsquo;d have decided to invest the extra money.&lt;/p&gt;&#10;&lt;p&gt;For my prototyping usage on the other hand the &lt;a href="https://blog.schallbert.de/en/portal-milling/#tool-changer"&gt;hypothesis I made in the past&lt;/a&gt; still holds true. I&amp;rsquo;m using a &lt;em&gt;lot&lt;/em&gt; of different endmills as I&amp;rsquo;m working with many materials that each require a unique set of tools. The cost of steep taper toolholders I&amp;rsquo;d require to cover my work would have exceeded the cost of the spindle.&lt;/p&gt;&#10;&lt;p&gt;Instead, I bought some more spindle nuts. This way, I can keep the most-often used collets attached to their nuts, reducing the time required for a tool change.&lt;/p&gt;&#10;&lt;h2 id="cost-"&gt;Cost 💶&lt;/h2&gt;&#10;&lt;p&gt;I made some other decisions to both match my way of intermittently operating the machine as a sideline and to keep cost down. In my spindle configuration, I selected the &lt;code&gt;1.5kW&lt;/code&gt; low power option and did not raise specs of the bearings so the machine is limited to &lt;code&gt;30kRPM&lt;/code&gt;. I chose forced air cooling over the water-cooled option and did not invest in a continuous temperature monitoring system.&lt;/p&gt;&#10;&lt;p&gt;Still, total system cost are more than 2x spindle cost. Note that although the VFD is not too expensive, shielded motor wiring, connectors, torque wrench, line filter, collets/nuts, the switchbox and many additional, small parts add up to that high of a number.&lt;/p&gt;&#10;&lt;h2 id="information-phase"&gt;Information phase&lt;/h2&gt;&#10;&lt;p&gt;To get the new spindle running I had to plan, build, and configure a complete subsystem that involves spindle drive, cooling, auxiliary power supply. Driven by the project complexity, I executed my work in different phases that would build on top of each other. Starting with the information phase, let&amp;rsquo;s go through some of the questions I had to find answers to.&lt;/p&gt;&#10;&lt;h3 id="variable-frequency-drive"&gt;Variable Frequency Drive&lt;/h3&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/spindleupgrade/switchbox.jpg" alt="Image: Switchbox with VFD"&gt;&lt;/figure&gt;&#10;As I wrote in &lt;a href="https://blog.schallbert.de/en/cnc-electronics/#vfd"&gt;this article&lt;/a&gt;, a VFD is used to control rotational speed of a HF-spindle. The motor is called like that because it operates well above grid frequency (&lt;code&gt;50Hz&lt;/code&gt; in my region) to reach higher rotation speeds as required by the application, especially when working with smaller endmills.&lt;/p&gt;&#10;&lt;p&gt;When I took a deep dive into that topic for my new spindle application, I had a phone call with Mr. Wagner from &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; (VFD manufacturer) to answer some of my questions.&lt;/p&gt;&#10;&lt;h3 id="uf-characteristic-or-sensorless-vector-control"&gt;&lt;code&gt;U/f characteristic&lt;/code&gt; or &lt;code&gt;sensorless vector control&lt;/code&gt;?&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Selecting &lt;code&gt;U/f characteristic&lt;/code&gt; will make the VFD reduce voltage along with the frequency as long as the motor operates below its corner frequency (which for my machine is &lt;code&gt;400Hz&lt;/code&gt;). This is because the inductance of the stator wires reduces with lower frequency. That in turn increases the current flow through the stator, and along with it torque and power dissipation rises. So in the end, by applying the &lt;code&gt;U/f characteristic&lt;/code&gt;, current and with it the torque is kept constant. At very slow speeds though, the inductance becomes so low in comparison to the stator resistance that torque decreases over-proportionally which makes this option a bad choice when operating in that low-speed area. At or above the motor&amp;rsquo;s corner frequency on the other hand, voltage is kept stable and just the frequency is increased which reduces torque.&lt;/li&gt;&#10;&lt;li&gt;With &lt;code&gt;sensorless vector control&lt;/code&gt;, the VFD uses a mathematical model of the motor it drives to calculate optimal switching times to always achieve nominal (or even above) motor torque independently of the motor&amp;rsquo;s rotational speed. It is especially useful for applications that require heavy starting like elevators etc. and often requires the user to perform a VFD auto-calibration run with the motor connected.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Here&amp;rsquo;s a link to somebody&amp;rsquo;s &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 which the two options are compared at low speeds. Here&amp;rsquo;s &lt;a href="https://www.youtube.com/watch?v=tWpChgsAIN4" target="_blank" rel="noopener noreferrer" class="external-link"&gt;another one&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; that demonstrates how the VFD even goes beyond nominal motor torque when load is applied.&lt;/p&gt;&#10;&lt;p&gt;As most of the endmills I use demand a speed of &lt;code&gt;15000rpm&lt;/code&gt; or higher, I likely won&amp;rsquo;t feel the drawbacks of the &lt;code&gt;U/f characteristic&lt;/code&gt;. Plus, on my VFD, the &lt;code&gt;sensorless vector control&lt;/code&gt; option is only available up to &lt;code&gt;400Hz&lt;/code&gt; so I would only be able to use 80% of the spindle&amp;rsquo;s speed range. Easy decision: &lt;code&gt;U/f&lt;/code&gt; it is.&lt;/p&gt;&#10;&lt;h3 id="freewheeling-brake-resistor-usage-or-energy-recovery-system"&gt;freewheeling, brake resistor usage or energy recovery system?&lt;/h3&gt;&#10;&lt;p&gt;For braking the motor, the VFD can be configured with different options. When allowing the motor to freewheel in standard setting, it may take a long time until which the spindle has stopped from full speed.&lt;/p&gt;&#10;&lt;p&gt;When a brake resistor is configured like I do with my spindle application, braking down takes two seconds only. I also configured the DC brake to completely bring the shaft to a halt.&lt;/p&gt;&#10;&lt;p&gt;For more powerful motors or applications that involve movement of high masses, an energy recovery system might make sense. It is connected to the VFD&amp;rsquo;s intermediate circuit and is able to supply power generated through the motor&amp;rsquo;s excess motion energy back to grid. To use this feature, an additional electrical device is necessary and compabibility of the VFD&amp;rsquo;s intermediate circuit with that device has to be made sure. This adds high cost to the system and only pays off when large amounts of energy can be recuperated.&lt;/p&gt;&#10;&lt;h3 id="which-modulation-freqency--clock-frequency-should-i-use"&gt;Which modulation freqency / clock frequency should I use?&lt;/h3&gt;&#10;&lt;p&gt;The clock / modulation / carrier frequency is the rate at which the power transistors within the VFD switch to provide an output voltage for the motor that resembles a sine wave. On my VFD, it can be configured in a range of &lt;code&gt;2-15kHz&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;To select an optimal frequency, a balance is to be found between multiple factors:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Transistor switching losses rise at higher frequencies.&lt;/li&gt;&#10;&lt;li&gt;Transistor on-state losses rise at lower frequencies.&lt;/li&gt;&#10;&lt;li&gt;The &lt;a href="https://en.wikipedia.org/wiki/Equal-loudness_contour" target="_blank" rel="noopener noreferrer" class="external-link"&gt;human ear is especially sensitive&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; in the range of &lt;code&gt;2-5kHz&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Resonance frequency of the motor system should be avoided.&lt;/li&gt;&#10;&lt;li&gt;Electromagnetic interference emissions rise at higher frequencies.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Of course, if your motor manufacturer makes a modulation frequency recommendation, you should follow that one. I played around with different values and chose &lt;code&gt;5kHz&lt;/code&gt; as a good compromise between the above factors. I also felt it sounded better than 6 or 4 kHz on my machine.&lt;/p&gt;&#10;&lt;h3 id="can-i-control-the-coolant-fan-based-on-spindle-temperature"&gt;Can I control the coolant fan based on spindle temperature?&lt;/h3&gt;&#10;&lt;p&gt;Yes, that is possible. All you need are a continuous (non-switching) type of temperature sensor, e.g. PT100 or other thermocouple, a measuring transmitter, and a free analog input of your VFD that can be configured to on-off control one of the free digital output ports that then switches a relay the cooling fan has to be connected to.&lt;/p&gt;&#10;&lt;p&gt;My motor, unfortunately, is equipped with a switching-type thermistor that connects to the VFD via a temperature alarm input. Thus it can shut the system down once an over-temperature event occurs but is not suitable for a continuous temperature monitoring/control of the cooling fan.&lt;/p&gt;&#10;&lt;h3 id="is-18v-enough-to-detect-high-on-a-24v-digital-input"&gt;Is &lt;code&gt;18V&lt;/code&gt; enough to detect &lt;code&gt;HIGH&lt;/code&gt; on a &lt;code&gt;24V&lt;/code&gt; digital input?&lt;/h3&gt;&#10;&lt;p&gt;Yes, on my VFD I was able to feed a lower voltage than the targeted 24V to a logic input and still make it detect &lt;code&gt;HIGH&lt;/code&gt; level.&lt;/p&gt;&#10;&lt;h3 id="can-i-connect-vfd-logic-ground-to-my-dc-supply-ground-without-issues"&gt;Can I connect VFD logic ground to my DC supply ground without issues?&lt;/h3&gt;&#10;&lt;p&gt;Yes. I did and there were no issues. I even had to do so because my &amp;lsquo;Run&amp;rsquo; input is potential-free so I had to connect it to the DC power supply&amp;rsquo;s positive voltage rail over a relay contact. Also my numerical controller&amp;rsquo;s analogue output voltage to control spindle speed is forced to use the same ground reference as the VFD, so it&amp;rsquo;s also connected to the VFD&amp;rsquo;s logic ground.&lt;/p&gt;&#10;&lt;h3 id="what-do-i-need-to-consider-regarding-electromagnetic-compatibility"&gt;What do I need to consider regarding electromagnetic compatibility?&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Use a line filter between mains switch and VFD.&lt;/li&gt;&#10;&lt;li&gt;Use shielded cables for the motor you connect.&lt;/li&gt;&#10;&lt;li&gt;Keep motor cable as short as possible.&lt;/li&gt;&#10;&lt;li&gt;If you have to use long cables, add a HF noise filter.&lt;/li&gt;&#10;&lt;li&gt;Have the shield connected to the VFD&amp;rsquo;s protective earth panel along with the motor&amp;rsquo;s protective earth leads.&lt;/li&gt;&#10;&lt;li&gt;Make sure the shield establishes conductive contact to the cable connectors.&lt;/li&gt;&#10;&lt;li&gt;Perform a continuity test.&lt;/li&gt;&#10;&lt;li&gt;Make sure the switchbox has one central protective earth clamp where every device within the switchbox connects to.&lt;/li&gt;&#10;&lt;li&gt;Make sure your supply&amp;rsquo;s protective earth is connected to that clamp, too.&lt;/li&gt;&#10;&lt;li&gt;When wiring the switchbox, position motor cables as far away as possible from low-voltage signal wires.&lt;/li&gt;&#10;&lt;li&gt;Use a metal switchbox housing (connected to the central earth point, of course) or at least a metallic carrier plate you mount all devices to. This will reduce radiated electromagnetic interferences.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="spindle"&gt;Spindle&lt;/h3&gt;&#10;&lt;p&gt;To select a matching spindle for your machine and application, there are a few points to consider - independently of the motor type.&lt;/p&gt;&#10;&lt;h3 id="power"&gt;Power&lt;/h3&gt;&#10;&lt;p&gt;The spindle power maybe is less important than you might think. I&amp;rsquo;ll give some examples you can follow to choose a good match.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;&amp;lt;=400W&lt;/code&gt; HF-Spindle or &lt;code&gt;&amp;lt;=800W&lt;/code&gt; router motor: You have a hobbyist machine and do not require to run it blazingly fast. It is fine for you to go shallow or medium depth per pass on hard to cut materials like aluminium or HPL. Your machine frame weighs less than &lt;code&gt;60kg&lt;/code&gt; and is relatively big for its weight, e.g. workbed surface of &lt;code&gt;&amp;gt;=0.5m²&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;&amp;lt;=700W&lt;/code&gt; HF-Spindle or &lt;code&gt;&amp;lt;=1200W&lt;/code&gt; router motor: Your machine weighs more or is more stiff than the above option. You use bits bigger than &lt;code&gt;6mm&lt;/code&gt; in diameter a lot, or have to machine parts with face-milling cutters often. Your Z-axis is strong and you feel your machine is bored when running through hard wood at &lt;code&gt;3000mm/min&lt;/code&gt;, &lt;code&gt;Z+6mm&lt;/code&gt; with a &lt;code&gt;6mm&lt;/code&gt; cutter.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;1.1kW / 1.5kW&lt;/code&gt; HF-Spindle: You have a compact machine (around &lt;code&gt;500x500mm&lt;/code&gt; travel, weight: &lt;code&gt;100kg&lt;/code&gt;) and do want it to finish its jobs quickly, cutting aluminium or wood at higher feed rates. When working with wood, you use roughers a lot to save time. Your machine can handle higher accelerations and rapid movements (e.g. &lt;code&gt;1000mm/s²&lt;/code&gt; and &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;: You left the hobby sector - likely forever - both with your machine and the order bank that has piled up on your desk. You have a workshop, a professional dust collection, and an heavier machine you trust to run all day long without intense supervision. You might consider this blog as &amp;ldquo;too basic&amp;rdquo; and head off to new shores where people can really help you as a professional.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="weight"&gt;Weight&lt;/h3&gt;&#10;&lt;p&gt;Weight scales with power. An &lt;code&gt;800W&lt;/code&gt; router motor can come at below &lt;code&gt;2kg&lt;/code&gt; including mount while a &lt;code&gt;2.2kW&lt;/code&gt; HF-Spindle with automatic tool change might be at or above &lt;code&gt;6.5kg&lt;/code&gt; without tool holder. Footprint and cables also tend to become larger.&#10;A high weight can negatively affect light machines, especially. In an extreme case, the Z-axis might start moving down when the stepper motor is not energized and machine acceleration, especially at the portal movement axis, might have to be reduced so no steps are lost. Also keep in mind that heavier machines tend to be bigger, so the cutting forces can have unfavorable effects on the Z-axis due to increased leverage.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/spindleupgrade/mafellvsspinogy.jpg" alt="Image: Comparison of Router motor to HF-Spindle"&gt;&lt;/figure&gt;&#10;&lt;h3 id="2-pole-or-4-pole-design"&gt;2-pole or 4-pole design?&lt;/h3&gt;&#10;&lt;p&gt;If you need a lot of torque, especially at low speeds, chose a 4-pole design. Keep in mind that you will require double the frequency to have it turn with the same RPM as a 2-pole motor.&#10;If you often run your machine at high RPM instead, consider buying a 2-pole spindle.&lt;/p&gt;&#10;&lt;h3 id="water-or-air-cooled"&gt;Water or air-cooled?&lt;/h3&gt;&#10;&lt;p&gt;This again is more a question of cost and professionality than anything else. Of couse, bolt-on fan self-cooling works fine. Forced-air cooling comes a little more expensive but is likely more quiet, its main advantage being that the spindle does not get too warm independently of its speed of rotation.&lt;/p&gt;&#10;&lt;p&gt;A water cooling solution removes heat from the motor more efficiently and the water&amp;rsquo;s high heat capacity leads to homogenous temperatures in the whole spindle. It dampenes vibrations and is the quietest cooling solution of all. On the other hand, the system is much more expensive, more complex, takes more space for pump, radiator and equalizing tank etc., and requires maintenance.&lt;/p&gt;&#10;&lt;h2 id="the-planning-phase"&gt;The planning phase&lt;/h2&gt;&#10;&lt;p&gt;After studying the manuals of all the parts I had ordered, I created a schematic circuit diagram for the spindle subsystem.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/spindleupgrade/vfd_circuit_plans_1.jpg" alt="Image: VFD subsystem circuit diagram"&gt;&lt;/figure&gt;&#10;&lt;p&gt;At the top you see the VFD with its terminals. &lt;code&gt;X1&lt;/code&gt; is the mains power input with disconnector switch &lt;code&gt;S2&lt;/code&gt;. &lt;code&gt;X2&lt;/code&gt; connects spindle to the VFD and &lt;code&gt;X3&lt;/code&gt; is the signal port to the CNC. In the lower right, there&amp;rsquo;s a DC power supply &lt;code&gt;T1&lt;/code&gt; that feeds the forced-air cooling and provides a logic voltage rail. The spindle &lt;code&gt;M1&lt;/code&gt; motor&amp;rsquo;s temperature is monitored by the thermistor &lt;code&gt;R3&lt;/code&gt; (where I forgot to add the label.) The spindle cooling motor &lt;code&gt;M2&lt;/code&gt; is controlled by a time relay which is in turn controlled through the &lt;code&gt;RUN&lt;/code&gt; signal. A digital multi-purpose output is connected to a red LED &lt;code&gt;D1&lt;/code&gt; that is configured to light up if an overload warning is present.&lt;/p&gt;&#10;&lt;p&gt;The circuit plan remains pretty straight-forward. So i did not invest time into neat drawings. To make it look even more authentic, I spilled some ☕ on the paper.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/spindleupgrade/vfd_circuit_plans_3.jpg" alt="Image: Spindle connector pinout"&gt;&lt;/figure&gt;&#10;&lt;p&gt;This is the pinout of the spindle connector &lt;code&gt;X2&lt;/code&gt; and its corresponding terminal on the VFD. It was useful several times when adding connectors to the shielded motor cable (orange in color, ref. to banner image at the top), and during unit testing.&lt;/p&gt;&#10;&lt;p&gt;I noted down the fastening torque requirements for each screw including the terminals and prepared the tools I&amp;rsquo;d require to both prepare the switchbox&amp;rsquo;s in- and outlets and air vents, as well as the mount and wiring setup.&lt;/p&gt;&#10;&lt;h2 id="the-construction-phase"&gt;The construction phase&lt;/h2&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&gt;&lt;iframe&#10; src="https://makertube.net/videos/embed/u8gDBQnsdxYFEZfFwk6jfU"&#10; title="Spindle VFD Cabinet build"&#10; loading="lazy"&#10; allow="autoplay; fullscreen; picture-in-picture"&#10; allowfullscreen&#10; style="width: 100%; height: auto; min-height: 315px; aspect-ratio: 16/9;"&#10; &gt;&lt;/iframe&gt;&lt;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Spindle VFD Cabinet build&lt;/span&gt;&lt;a href="https://makertube.net/videos/embed/u8gDBQnsdxYFEZfFwk6jfU" class="attr-link" target="_blank" rel="noopener noreferrer"&gt;↗&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="commisioning"&gt;Commisioning&lt;/h2&gt;&#10;&lt;p&gt;Before taking the system into productive use, I ran the &lt;a href="https://blog.schallbert.de/en/testing-hardware/#test-plan"&gt;test plan&lt;/a&gt;. When it was complete, I executed a Spindle grease distribution run according to the manufacturer&amp;rsquo;s instructions.&#10;Then, I calibrated the &lt;code&gt;0-10V&lt;/code&gt; analog out in my CNC software to match VFD frequency / spindle RPM. Example: &lt;code&gt;S10000&lt;/code&gt; should generate &lt;code&gt;3.33V&lt;/code&gt; at the output and make the VFD run at a frequency of around &lt;code&gt;175Hz&lt;/code&gt; to account for slip.&lt;/p&gt;&#10;&lt;p&gt;Finally, I was able to run some light jobs to see that the system works as intended: &lt;code&gt;8mm&lt;/code&gt; PMMA, full slot, single pass in &lt;code&gt;Z+=8mm&lt;/code&gt;, &lt;code&gt;4mm&lt;/code&gt; single flute (polished) carbide cutter at &lt;code&gt;S=26kRPM&lt;/code&gt; and &lt;code&gt;F=3000mmm/min&lt;/code&gt;&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/spindleupgrade/spindle_commisioning_initjob.jpg" alt="Image: Spindle running a workpiece of PMMA"&gt;&lt;/figure&gt;&#10;&lt;h2 id="the-analysis-phase"&gt;The analysis phase&lt;/h2&gt;&#10;&lt;p&gt;This phase will follow later, maybe a year into working with the new spindle. I&amp;rsquo;ll have a look at the following:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Was the investment worth it (roughly 8x more expensive than a router motor)?&lt;/li&gt;&#10;&lt;li&gt;Did I use the additional capabilities (higher power &amp;amp; more RPM)?&lt;/li&gt;&#10;&lt;li&gt;Does it produce noticeably better quality results?&lt;/li&gt;&#10;&lt;li&gt;Is it more reliable and requires less maintenance to my previous motor?&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;But what I already now can say: I learned a lot, it was fun designing, wiring, setting up the system, and exciting to actually see it worked out as I intended 🙂&lt;/p&gt;&#10;</description></item><item><title>Spirograph</title><link>https://blog.schallbert.de/en/projects/spirograph/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/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="Image: Acrylic glass spirograph made on my CNC &amp;#39;Zerspanobert&amp;#39;"&#10; title="Spirograph" /&gt;&#10;&lt;h2 id="the-spirograph"&gt;The spirograph&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Spirograph" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Spirographs&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; are mathematical toys. With their help it is easy to draw repetitive, curved shapes. The simplest version is made up of a cogwheel that runs on the inside of a static ring gear. The running wheel has a couple of holes to take the tip of a pen. The holes are located out of center of that cogwheel so, when rotating the wheel inside the hollow one, it will create parabolic shapes.&lt;/p&gt;&#10;&lt;p&gt;The number of rotations needed to draw a closed shape depends on the relation of tooth count between the two wheels.&lt;/p&gt;&#10;&lt;h3 id="version-0"&gt;Version 0&lt;/h3&gt;&#10;&lt;p&gt;Our initial design was created together with the guys from 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 the workshop&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. It is a 14:9 tooth combination, so the lowest common multiple (aka LCM) is 126 which would create tight drawings with &lt;code&gt;126 / 9 = 14&lt;/code&gt; corners. We had it manufactured in Aluminium and discussed about a good milling strategy using the CAM part of their software.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&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;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Milling Spirograph_V0 on an overclocked CompactLine CNC&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;The parts turned out good and the wheels would allow cog to cog placement but unfortunately, it wouldn&amp;rsquo;t turn because the inner wheel&amp;rsquo;s next cog wouldn&amp;rsquo;t hit the flank but instead the ring gear tooth&amp;rsquo;s head.&lt;/p&gt;&#10;&lt;h3 id="version-1"&gt;Version 1&lt;/h3&gt;&#10;&lt;p&gt;When the workshop was over, I took the design home and tried to improve. I shortened the tooth heads of the outer wheel to fix the previous issue and had some rounds added so the wheels wouldn&amp;rsquo;t feel edgy.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/spirograph/V1_upper.jpg" alt="Image: Spirograph in Beech, upper side milling complete"&gt;&lt;/figure&gt;&#10;I had the design manufactured in 18mm Beech glued wood. I introduced two-sided milling here because chamfering by hand turned out to be a bit tedious at these more complex shapes. For that, I did not cut right through the material but left a 1mm &amp;ldquo;onion skin&amp;rdquo; at the bottom which would hold everything in place.&lt;/p&gt;&#10;&lt;p&gt;When I finally had the edges of the lower side chamfered, the bit would pierce the onion skin and practically cut out the workpieces so that I&amp;rsquo;d just have to snatch them off the machine bed. I just had to sand down the burrs which took less than five minutes of manual rework.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/spirograph/V1_detail.jpg" alt="Image: Spirograph_V1 in Beech, detail view of cogs that wouldn&amp;#39;t fit"&gt;&lt;/figure&gt;&#10;But it still wouldn&amp;rsquo;t fit. This time, because the flanks of the inner wheel were too wide. As a consequence, they would not allow full engage depth which in turn makes the next tooth of the inner wheel hit the tooth&amp;rsquo;s head of the ring gear.&lt;/p&gt;&#10;&lt;h3 id="version-2"&gt;Version 2&lt;/h3&gt;&#10;&lt;p&gt;I figured out that in Version_1, the inner wheel had a positive &lt;code&gt;tooth profile shift value&lt;/code&gt; while the ring wheel was left at zero. This couldn&amp;rsquo;t work at all so I fixed it. I also reworked both wheel&amp;rsquo;s concave radiuses so a 6mm endmill would be able to properly carve all details. Finally, I added some play for the teeth flanks and allowed for more room in the teeth&amp;rsquo;s troughs for better tooth meshing.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/spirograph/V2_pmma.jpg" alt="Image: Spirograph_V2 in PMMA, along with sample drawings"&gt;&lt;/figure&gt;&#10;To give more room to the inner wheel, I mutated the gear combination to 16/7 that will draw &lt;code&gt;112 / 7 = 16&lt;/code&gt; corners (even tighter drawing).&lt;/p&gt;&#10;&lt;p&gt;This time I had it manufactured in 5mm extruded Acrylic (PMMA), opaque white. I went with a similar two-sided milling strategy as discussed above, but I added a finishing run for superior quality of cut.&#10;I used the following parameters:&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;Cutter, Material: PMMA (Acrylic)&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Work type&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Tooth&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Dia [mm]&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Speed [RPM]&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Feed [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;carbide hawk beak upcut FSECO1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Roughing&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;carbide hawk beak upcut FSAC&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Finishing&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;carbide taper 90° FEF&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Chamfers&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;And it worked flawlessly 😄, quality of cut was very high and all I did manually was to use a scraper for deburring that took under a minute.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center media-frame--video"&gt;&#10; &lt;div class="media-video"&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;/div&gt;&#10; &lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Updated tooth parameters allow smooth meshing&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;But still there was room to improve:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Holes right in the teeth don&amp;rsquo;t make sense because the inner wheel would not want to turn when engaged&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;5mm&lt;/code&gt; holes are a bit too small to home all sorts of crayons&lt;/li&gt;&#10;&lt;li&gt;16 teeth modul10 are not enough to fill a DIN A4 sheet of paper&lt;/li&gt;&#10;&lt;li&gt;The elliptic shape looks nice but is a bit hard to keep in place&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;That&amp;rsquo;s why I made one more step in the evolution:&lt;/p&gt;&#10;&lt;h3 id="version-3"&gt;Version 3&lt;/h3&gt;&#10;&lt;p&gt;I made another slight update over Version_2, this time with an 18/10 pairing (LCM of 90, so 9 corners). I reworked the hole distances in form of a fibonacci spiral to yield more expressive designs. Finally, I updated the outline to match DIN A4 paper size so that the designs now would be centered.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/spirograph/V3_design.jpg" alt="Image: Spirograph_V3 design"&gt;&lt;/figure&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/spirograph/V3_detail.jpg" alt="Image: Spirograph_V3 in PMMA, detail view of a tooth"&gt;&lt;/figure&gt;&#10;I used the same parameters as in the previous version for manufacturing. See for yourself how good it came out. I wonder if I can get the last 5% out of quality by avoiding burrs altogether. Alone, I wouldn&amp;rsquo;t knnow how. Looks like the burr is created when the tapered cutter pierces the underside of the material to make the final cutout on my vacuum table.&lt;/p&gt;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/spirograph/V3_chattermarks_acceleration.jpg" alt="Image: Spirograph_V3, contour detail view"&gt;&lt;/figure&gt;&#10;Another thing I noticed is that the machine creates chatter marks when accelerating to target speed in &lt;code&gt;G01&lt;/code&gt; operations. I guess this is due to the limited frame stiffness. It is noticeable only under a few angles but not palpable.&lt;/p&gt;&#10;&lt;p&gt;The finished product then looks like this. Due to the chamfers and rounded edges, it feels really smooth. The gears run effortlessly the 18/10 cog combination has so many teeth meshed at all times that even kids aged 5 have no issue to produce perfect shapes (tested it).&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/spirograph/V3_pmma.jpg" alt="Image: Spirograph_V3 machined in Acrylic"&gt;&lt;/figure&gt;&#10;&lt;p&gt;And finally, I&amp;rsquo;m also happy with how the drawings look. Bold, big enough (&lt;code&gt;15x15cm&lt;/code&gt; / &lt;code&gt;6&amp;quot; by 6&amp;quot;&lt;/code&gt;). Lines stand together not too tight. The higher number of holes that the larger inner wheel allows help generating higher diversity figures as well.&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/spirograph/V3_drawing.jpg" alt="Image: Drawing sample with Spirograph_V3"&gt;&lt;/figure&gt;&#10;&lt;p&gt;I will donate the specimen to a nearby school. Let&amp;rsquo;s see how they use them.&lt;/p&gt;&#10;&lt;h2 id="you-want-to-diy"&gt;You want to DIY?&lt;/h2&gt;&#10;&lt;p&gt;No problem. &lt;a href="https://blog.schallbert.de/assets/docs/spirograph_V3_by_schallbert.zip"&gt;Download a &lt;code&gt;ZIP&lt;/code&gt; package here&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="whats-in-the-package"&gt;What&amp;rsquo;s in the package?&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; project that allows all sorts of wheel pairing configurations&lt;/li&gt;&#10;&lt;li&gt;Drawing files (&lt;code&gt;DXF&lt;/code&gt;) both for front and rear milling&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; project files along with my tool database&lt;/li&gt;&#10;&lt;li&gt;CNC operations files (&lt;code&gt;tap&lt;/code&gt;) that fit my machine&lt;/li&gt;&#10;&lt;li&gt;A readme (&lt;code&gt;txt&lt;/code&gt;) that defines XY zero and required offsets&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="does-it-cost-anything"&gt;Does it cost anything?&lt;/h3&gt;&#10;&lt;p&gt;No, it&amp;rsquo;s for free. Note that the License I use makes the package &amp;ldquo;free software&amp;rdquo;. Please contact me if you want to utilize anything of the designs in other contexts than home and private usage.&lt;/p&gt;&#10;&lt;h3 id="disclaimer"&gt;Disclaimer&lt;/h3&gt;&#10;&lt;p&gt;As usual, the files are available for download free of charge. In turn, I do not take any responsibility or liability for their contents.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;WARNING&lt;/strong&gt; ⚠️&lt;/p&gt;&#10;&lt;p&gt;These files provide or allow output of G-code instructions for real machinery that does actually do things in the physical world. There might be code errors or bugs that could potentially lead to machine crashes or even worse. I do not take liability for work accidents, system failures, equipment breakdown, loss of production, flow disturbances, or other negative effects that may be caused through the files I provided.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt; ℹ️&lt;/p&gt;&#10;&lt;p&gt;It is the machine operator&amp;rsquo;s responsibility to carefully review G-code and to make sure that it works as intended on the specific machine without causing any harm.&lt;/p&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; More Spirographs&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2024-02-05T00:00:00Z"&gt;&#10; 2024-02-05&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; I made a few more very nice spirographs for the Maker Faire in early 2024. The base material is medium-density fiberboard (MDF), onto which I applied a layer of olive wood veneer.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;figure class="media-frame media-frame--center"&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;</description></item><item><title>This site Demo</title><link>https://blog.schallbert.de/en/projects/thissite/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/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="Schallbert&amp;#39;s Desktop"&#10; title="This site Demo" /&gt;&#10;&lt;h2 id="project-stats"&gt;Project stats&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Difficulty: medium 3/5&lt;/li&gt;&#10;&lt;li&gt;Cost: 0€&lt;/li&gt;&#10;&lt;li&gt;Time: ~15h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="the-idea"&gt;The idea&lt;/h2&gt;&#10;&lt;p&gt;The idea originated through my &lt;a href="https://blog.schallbert.de/en/projects/mobfobamp/"&gt;MobFobAmp&lt;/a&gt; project of which I didn&amp;rsquo;t have a github repository as there was no code or similar documentation to keep. So I decided I wanted a website so I could host instructions for this Open Hardware project for others to build their own copy. I wanted an easy-to-host solution that wouldn&amp;rsquo;t take too much work to set up. As I don&amp;rsquo;t have an own server, I asked for help. &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; recommended to use github pages as a site host to start with for easy auto-deployment and global scope. So I gave it a try.&lt;/p&gt;&#10;&lt;h2 id="getting-started"&gt;Getting started&lt;/h2&gt;&#10;&lt;p&gt;I figured out that a static site would be the right thing to use, without the need for a database or huge Content Management frameworks as my site layout should be simple and minimalistic; I just wanted to host this one project in the beginning.&#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 recommended to use Jekyll&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://upload.wikimedia.org/wikipedia/commons/4/42/Jekyll_%28software%29_Logo.png" alt="Jekyll Logo"&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Jekyll Logo&lt;/span&gt;&lt;a&#10; href="https://commons.wikimedia.org/wiki/File:Jekyll_%28software%29_Logo.png#file"&#10; class="attr-link"&#10; aria-label="Attribution 1"&#10; &gt;&#10; &lt;sup class="attr-id"&gt;[1]&lt;/sup&gt;&#10; &lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;as a site generator. I had some trouble with the Windows Subsystem for Linux (WSL) as it wouldn&amp;rsquo;t accept my terminal inputs at some point, so I followed the &lt;a href="https://jekyllrb.com/docs/installation/windows/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;guide&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; and used the RubyInstaller which worked right out of the box for me.&lt;/p&gt;&#10;&lt;h2 id="site-setup"&gt;Site setup&lt;/h2&gt;&#10;&lt;p&gt;I realized that I might want to document more of my projects, and not only the &amp;ldquo;hardware&amp;rdquo; ones. That&amp;rsquo;s why I came up with the idea of grouping projects together, each with a header, a little teaser text, and a thumbnail image. Clicking on which would lead to the individual project page. Which would involve a lot of programming for page setup and layouting which I didn&amp;rsquo;t want to spend too much time on - my other projects were waiting&amp;hellip;&lt;/p&gt;&#10;&lt;h2 id="first-try-with-themes"&gt;First try with Themes&lt;/h2&gt;&#10;&lt;p&gt;So I followed another suggestion by Github pages: The usage of pre-defined &amp;ldquo;Themes&amp;rdquo;.&#10;I first used the &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;. This one has a great landing page with a huge picture and it supports collections that enable the group-the-project thing that I wanted. Unfortunately, the theme is a &amp;ldquo;remote theme&amp;rdquo; so customization was limited, e.g. I didn&amp;rsquo;t manage to have a colletion point to another page, it would always open a &amp;ldquo;popup&amp;rdquo; and I just didn&amp;rsquo;t want this. (Later, I figured out that remote layouts can be overridden, but that was too late so I already switched to another theme supplier&amp;hellip;)&#10;The theme I&amp;rsquo;m currently using the Minimal Mistakes Theme (see site footer) which is very well documented. After downloading and a couple of first steps, I struggled with custom colors (_sass) and the collections - again - as I didn&amp;rsquo;t understand at first how Jenkill would do the bindings behind the scenes. Some of my struggles can be found in the Blog posts from June 2021.&lt;/p&gt;&#10;&lt;h3 id="site-layouts-hero-screens"&gt;Site layouts: hero screens&lt;/h3&gt;&#10;&lt;p&gt;A bit of thinking should go into how the site layout should be like, to make navigation easy and intuitive. The approach will differ with every page type due to differences in complexity and document structure. &lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/thissite/hero_landing.jpg" alt="landing hero"&gt;&lt;/figure&gt;Take the landing / home page for example, I want the navigation to be sticky so that the site&amp;rsquo;s categories are always visible. It shall receive a huge, wide overlay image with the page description. Below, latest posts will be listed.&#10;This is the example for the collection&amp;rsquo;s page hero screen: &lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/thissite/hero_collection.jpg" alt="Image: collection hero"&gt;&lt;/figure&gt;&#10;The projects and their contents shall contain a page navigation in the left sidebar, may contain a scrollable header image, and has a wide setting to display a high amount of text in the content area. &lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/thissite/hero_project.jpg" alt="Image: project hero"&gt;&lt;/figure&gt;&lt;/p&gt;&#10;&lt;p&gt;The &amp;ldquo;hero screen&amp;rdquo; scetches for this site&amp;rsquo;s layouts can be found &lt;a href="https://blog.schallbert.de/assets/docs/site_hero_screens.pdf"&gt;here&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="collections"&gt;Collections&lt;/h3&gt;&#10;&lt;p&gt;Collections are a great way for grouping content. Look at my &lt;a href="https://blog.schallbert.de/en/projects/"&gt;Open Hardware Projects&lt;/a&gt; page. You can find all projects in this category with a little teaser image and description here to get an overview.&lt;/p&gt;&#10;&lt;p&gt;This is how the collection&amp;rsquo;s configuration can work out:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Think of a fitting name for your collection, e.g. &lt;code&gt;electronics&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Collection name is defined in &lt;code&gt;_config.yml&lt;/code&gt; as &lt;code&gt;collections: electronics&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Collection name has to be set in &lt;code&gt;#defaults&lt;/code&gt; as &lt;code&gt;type: electronics&lt;/code&gt; so that Jekyll knows how to interpret the actual content.&lt;/li&gt;&#10;&lt;li&gt;Collection page has to be created (e.g. in the &lt;code&gt;_pages&lt;/code&gt; folder) with the attribute &lt;code&gt;collection: electronics&lt;/code&gt;. This is the page that will actually display the overview of items in the collection.&lt;/li&gt;&#10;&lt;li&gt;Content folder with the same name as the collection&amp;rsquo;s name with leading underscore has to be created: &lt;code&gt;_electronics&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;All documents in this content folder will show up in the collection page automatically after a full build.&lt;/li&gt;&#10;&lt;li&gt;These documents should get a &amp;ldquo;front matter&amp;rdquo; (section before the actual content in the file, marked with &lt;code&gt;---&lt;/code&gt;) so that they can be displayed as described in the corresponding &lt;code&gt;_layouts&lt;/code&gt; file, e.g. &lt;code&gt;collection&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="more-than-the-standard-theme"&gt;More than the standard Theme&lt;/h2&gt;&#10;&lt;p&gt;I have overridden some theme default and I&amp;rsquo;m using most of the existing Theme infrastructure. But my site has a couple of special needs so I had to customize and add Theme features. These are described in the following sections.&lt;/p&gt;&#10;&lt;h3 id="navigation"&gt;Sidebar: site navigation&lt;/h3&gt;&#10;&lt;p&gt;As my site navigation tree is pretty flat and I don&amp;rsquo;t have lots of pages to navigate to, I wanted the left sidebar to hold the page navigation rather than the site navigation. Thus, I wouldn&amp;rsquo;t need a right sidebar at all, so that there would be more space for the page content.&lt;/p&gt;&#10;&lt;h3 id="table-of-contents-on-the-left"&gt;Table Of Contents on the left&lt;/h3&gt;&#10;&lt;p&gt;As written &lt;a href="https://blog.schallbert.de/en/jekyll-toc/"&gt;here&lt;/a&gt; and &lt;a href="https://blog.schallbert.de/en/jekyll-toc/"&gt;there&lt;/a&gt;, I never was 100% happy with the way my project pages looked like: &lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2021-06-17_before_tocleft.jpg" alt="Image: project page before I setup toc_left"&gt;&lt;/figure&gt;&#10;The original idea of the theme is that the content is framed by two sidebars: The left one can either display Author information, custom content, or the site navigation. The content area is configurable between wide and normal, in the latter case leaving space for a second sidebar on the right. The right sidebar is configurable to not be there at all or to show the page&amp;rsquo;s table of contents.&lt;/p&gt;&#10;&lt;p&gt;So the target I set was to eliminate the right sidebar for my design, and instead of having the site navigation in the left sidebar, move the page navigation there.&lt;/p&gt;&#10;&lt;h3 id="sidebar"&gt;Where the sidebar design is defined&lt;/h3&gt;&#10;&lt;p&gt;It&amp;rsquo;s all about the &lt;code&gt;/_layouts&lt;/code&gt; folder that keeps information about how a page layout should look like. The page design in question is called &lt;code&gt;single.html&lt;/code&gt; and it at some point calls&#10;&lt;code&gt;include sidebar.html&lt;/code&gt;&#10;which resides in &lt;code&gt;/_includes/sidebar.html&lt;/code&gt;. So I thought of a new value I could use to control showing the table of contents within the left sidebar, chose &lt;code&gt;toc_left&lt;/code&gt; and added a condition:&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;(By the way: I had to comment-out and add &lt;code&gt;.&lt;/code&gt;&amp;rsquo;s to the &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, &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; would otherwise interpret these as commands!)&lt;/p&gt;&#10;&lt;h3 id="implementing-the-table-of-contents"&gt;Implementing the Table Of Contents&lt;/h3&gt;&#10;&lt;p&gt;I wanted it to show up in the left sidebar container. So I just had to create a &lt;strong&gt;toc_left&lt;/strong&gt; file that would call the included toc generator like this:&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-embeds"&gt;Audio Embeds&lt;/h3&gt;&#10;&lt;p&gt;Audio embeds are these little player-like things on a website, often forwarding to a streaming platform or similar. I need them for some of my audio projects like this &lt;a href="https://blog.schallbert.de/en/projects/mobfobamp/"&gt;mobfobamp&lt;/a&gt;: just a caption and the player below. &lt;figure class="media-frame media-frame--right"&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;a href="https://stackoverflow.com/a/63807971/13757172" target="_blank" rel="noopener noreferrer" class="external-link"&gt;This post&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; helped me a lot, so I could just add it to the &lt;code&gt;_include&lt;/code&gt;, modify it a bit, and it worked!&lt;/p&gt;&#10;&lt;h3 id="favicon"&gt;Placing the favicon&lt;/h3&gt;&#10;&lt;p&gt;I followed &lt;a href="https://ptc-it.de/add-favicon-to-mm-jekyll-site" target="_blank" rel="noopener noreferrer" class="external-link"&gt;this guide&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; to place a favicon for the site and it worked flawlessly. &lt;figure class="media-frame media-frame--center"&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;The favicon&amp;rsquo;s path has to be valid independently of the folder the rendered page resides in. I had to add &lt;code&gt;../&lt;/code&gt; according to the pages&amp;rsquo; depth within the site&amp;rsquo;s folder structure to make sure that the favicon could be displayed not only on the &amp;ldquo;home&amp;rdquo; page, but also from sub-paSges.&lt;/p&gt;&#10;&lt;h3 id="optimizing-for-search-engines"&gt;Optimizing for search engines&lt;/h3&gt;&#10;&lt;p&gt;As my site was not found by search engines for a couple of weeks, I decided to perform so-called &amp;ldquo;SEO&amp;rdquo; (search engine optimization). That&amp;rsquo;s why I added &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; and e.g. reworked all my links following &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;. When complete, I put my webpage through Lighthouse&amp;rsquo;s web-analysis audit. &lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/thissite/lighthouse_audit.jpg" alt="Image: Lighthouse Audit results"&gt;&lt;/figure&gt;&#10;For a first try, I think it looks pretty well. Biggest issue on the performance side seems to be CSS overhead.&lt;/p&gt;&#10;&lt;h3 id="content-security-policy"&gt;Content Security Policy&lt;/h3&gt;&#10;&lt;p&gt;The Lighthouse audit revealed that it would be a good idea to restrict external scripting to my site, so I introduced a Content Security Policy (CSP) like this:&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&amp;#39;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Initially, unfortunately, my home page&amp;rsquo;s backup image wouldn&amp;rsquo;t show, my embed-audio files wouldn&amp;rsquo;t play and all icons loaded externally were gone as well. Adding &lt;code&gt;media-src: 'self';&lt;/code&gt; and some more reading 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; and especially &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;Lighthouse&amp;rsquo;s recommendation&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; helped to solve this. By using the Browser&amp;rsquo;s &lt;em&gt;Web developer Tools&lt;/em&gt;, I figured out the missing resources the theme is loading in the background, so my updated CSP looks like this:&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;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Update: No more third parties&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2021-12-05T00:00:00Z"&gt;&#10; 2021-12-05&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; By the end of 2021, I chose to not have the page user download external resources from Wikimedia or FontAwesome, but decided to serve that content locally. This way, there&amp;rsquo;s no third party anymore registering calls to their content - plus, my page load times drop a little. I was able to revert changes to my Content Security Policy to the first example (but added &lt;code&gt;media-src 'self';&lt;/code&gt;) above without side effects.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;h2 id="errors"&gt;Errors&lt;/h2&gt;&#10;&lt;p&gt;I&amp;rsquo;m collecting some simple errors here that cost me quite some time fixing.&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;figure class="media-frame media-frame--right"&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;You enter &lt;code&gt;bundle exec jekyll serve&lt;/code&gt; and get a strange &lt;code&gt;UTF-8&lt;/code&gt; &amp;ldquo;incompatible character encoding&amp;rdquo; error? The cause is simple: One of your files most likely has no header that can be converted into the front matter of the page that is generated from that file. Just add your usual header to your file like so and it will just work:&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-yaml" data-lang="yaml"&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="webpage-is-not-being-displayed-at-all-404"&gt;Webpage is not being displayed at all: &lt;code&gt;404&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;This error was very, very hidden. I found it by accident browsing my site online only, as &lt;code&gt;localhost&lt;/code&gt; serve was not affected. The symptom is a no-show blank page.&lt;/p&gt;&#10;&lt;p&gt;Cause: Due to translation, my paths are getting longer and I wanted subpages to not have extremely long links. That&amp;rsquo;s why I was using the &lt;strong&gt;permalink:&lt;/strong&gt; Attribute of the pages&amp;rsquo; fontmatter. Now, only if you have a &amp;ldquo;trailing slash&amp;rdquo; here, the page will display on the remote server.&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;# Wrong:&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;# Correct:&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;# Unfolds on the english blog to:&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;# /en/projects/nopageerror/&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="site-hosting"&gt;Site hosting&lt;/h2&gt;&#10;&lt;p&gt;This site itself is a normal Gitea repository. It is built automatically with Gitea Actions once changes are added to the repository&amp;rsquo;s main branch.&lt;/p&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; From Github Pages to Gitea &amp;#43; Caddy&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2024-01-13T00:00:00Z"&gt;&#10; 2024-01-13&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; I &lt;a href="https://blog.schallbert.de/en/projects/move-blog-to-own-server/"&gt;moved my site&lt;/a&gt; from Github Pages to Gitea. I&amp;rsquo;m self-hosting both Gitea and the site now. Still, the site content is available as a repository and I can easily update and maintain it where I deem fit.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;&#10;&lt;p&gt;To get familiar with how Jekyll creates the site, how the theme would interact with my commands and how to override Theme behavior cost quite a couple of hours, but I think this site has a clear structure and a more or less &amp;ldquo;professional&amp;rdquo; look - And I didn&amp;rsquo;t have to write a single line of JavaScript or HTML to get this done. Nice!&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Contents written in markdown &lt;code&gt;*.md&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Basic site configuration in YAML &lt;code&gt;_config.yml&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Page layouts written in html and jinja &lt;code&gt;*.html&lt;/code&gt;, placed in &lt;code&gt;_layouts&lt;/code&gt; and &lt;code&gt;_includes&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Page variables and appearance in SASS &lt;code&gt;*.scss&lt;/code&gt;, automatically generating css files&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="contribute"&gt;Contribute?&lt;/h2&gt;&#10;&lt;p&gt;You have ideas, criticism, or interesting stuff you&amp;rsquo;d like to let me know? Get started on my &lt;a href="https://github.com/Schallbert/schallbert.github.io/discussions" 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; page!&lt;/p&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; From Jekyll to Hugo&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2026-09-06T00:00:00Z"&gt;&#10; 2026-09-06&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; In the summer of 2026, I migrated this website to a &lt;a href="https://blog.schallbert.de/en/announcements/blog-anniversary-hugo/"&gt;new technical platform&lt;/a&gt;. I now use the Hugo site generator. An overview of the changes can be found in my &lt;a href="https://blog.schallbert.de/en/projects/migrating-jekyll-to-hugo/"&gt;project on the migration&lt;/a&gt;.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;</description></item><item><title>Tonuino - alternative Firmware</title><link>https://blog.schallbert.de/en/projects/tonuino/</link><pubDate>Mon, 01 Jan 0001</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/en/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="Tonuino as a Bird&amp;#39;s house"&#10; title="Tonuino - alternative Firmware" /&gt;&#10;&lt;h2 id="project-stats"&gt;Project stats&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Difficulty: hard 5/5&lt;/li&gt;&#10;&lt;li&gt;Cost: 0€&lt;/li&gt;&#10;&lt;li&gt;Time: ~400h&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="impressions"&gt;Impressions&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&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-0"&#10; 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&#10; src="https://blog.schallbert.de/assets/images/tonuino/birdshouse_front.jpg"&#10; 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."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-1"&#10; 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&#10; src="https://blog.schallbert.de/assets/images/tonuino/birdshouse_inside.jpg"&#10; 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."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-2"&#10; 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&#10; src="https://blog.schallbert.de/assets/images/tonuino/guitaramp_top.jpg"&#10; 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."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;a&#10; class="hugo-gallery__thumb"&#10; href="#gallery-full-3"&#10; 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&#10; src="https://blog.schallbert.de/assets/images/tonuino/guitaramp_inside.jpg"&#10; 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."&#10; class="hugo-gallery__thumb-img"&#10; loading="lazy"&gt;&#10; &lt;/a&gt;&lt;/div&gt;&lt;div class="hugo-gallery__caption"&gt;&#10; My alternative tonuino-Firmware in action.&#10; &lt;/div&gt;&lt;/div&gt;&#10;&#10; &lt;div class="hugo-gallery__full" aria-live="polite"&gt;&lt;figure&#10; id="gallery-full-0"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/tonuino/birdshouse_front.jpg"&#10; 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."&gt;&lt;figcaption&gt;NFC-Tags are birds that can be placed in the bird&amp;#39;s house to play an album.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-1"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/tonuino/birdshouse_inside.jpg"&#10; 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."&gt;&lt;figcaption&gt;A microcontroller implements my encoder library to run this buttonless design of Tonuino.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-2"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/tonuino/guitaramp_top.jpg"&#10; 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."&gt;&lt;figcaption&gt;Tonuino in an old battery-powered guitar amp.&lt;/figcaption&gt;&lt;/figure&gt;&lt;figure&#10; id="gallery-full-3"&#10; class="hugo-gallery__figure media-frame media-frame--center"&gt;&#10; &lt;img&#10; src="https://blog.schallbert.de/assets/images/tonuino/guitaramp_inside.jpg"&#10; 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."&gt;&lt;figcaption&gt;The player is controlled via three buttons that I took from an old computer keyboard.&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&#10;&lt;/section&gt;&#10;&#10;&lt;p&gt;&lt;figure class="media-frame media-frame--right"&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;A software written for the Arduino Framework that controls an Mp3 Player (with SD card), an NFC Tag Reader, and provides user controls that together work like a jukebox that can be controlled via NFC Tag.&#10;For example, an NFC tag can be linked to a folder on the SD card containing audio files. The tag will also store information about the requested play mode for this album, e.g. &amp;ldquo;Random&amp;rdquo;. Once a linked tag is placed on this jukebox, the configured folder will play with the selected playmode.&#10;User controls allow to select the track, change volume and even to navigate a voice menu that allows deleting a tag, linking/ configuring a tag, and to lock/ unlock the input keys if required.&lt;/p&gt;&#10;&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;&#10;&lt;p&gt;Idea and fantastic execution found on &lt;a href="https://www.voss.earth/tonuino" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Thorsten Voß&amp;rsquo;s blog&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. It began with a feature (&lt;a href="https://blog.schallbert.de/en/projects/encoder/"&gt;encoder support&lt;/a&gt;) that I wanted to add. Unfortunately, the original code was a single-file application with several thousand lines of code and poor readabilty, so I had a hard time getting my changes in. So I re-wrote the code from scratch for better structure, readability, maintainability, and extensibility. By doing this, in parallel I read some books about Object Oriented design in C++, &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; and &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; and applied some where I tought they&amp;rsquo;d fit in. This way, I was able to both improve modularity and readability of the code, plus I taught myself how to write code in a &amp;ldquo;bigger&amp;rdquo; project.&lt;/p&gt;&#10;&lt;h2 id="hurdles-to-overcome"&gt;Hurdles to overcome&lt;/h2&gt;&#10;&lt;p&gt;This was first real OO C++ project, and, maybe, it was a little big to start with. All the new-to-me concepts, writing to an interface, first-time usage of &lt;a href="https://platformio.org" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Platformio as IDE&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, utilizing the &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;, using coding patterns like &lt;a href="https://www.oodesign.com/factory-pattern.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;factory&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; or &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; took nearly a year of evenings before this project could be completed. And there&amp;rsquo;s always something left to refactor and to improve.&lt;/p&gt;&#10;&lt;h2 id="quick-facts"&gt;Quick facts&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Unit test suite of 250+ test cases&lt;/li&gt;&#10;&lt;li&gt;Serial debug output configurable&lt;/li&gt;&#10;&lt;li&gt;Loosely coupled C++ OO architecture&lt;/li&gt;&#10;&lt;li&gt;Individual modules with clear tasks, scaleable and easily accessible for future feature add&lt;/li&gt;&#10;&lt;li&gt;custom Hardware abstraction layer; most of the code should be portable to other mcus without changes&lt;/li&gt;&#10;&lt;li&gt;custom Dependency Injection framework (Loader class)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="features"&gt;Features&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Configurable user input (buttons or encoder)&lt;/li&gt;&#10;&lt;li&gt;Auto-Poweroff when no button pressed for a configurable time&lt;/li&gt;&#10;&lt;li&gt;Power on via &amp;ldquo;play&amp;rdquo; button press&lt;/li&gt;&#10;&lt;li&gt;Autoplay feature&lt;/li&gt;&#10;&lt;li&gt;Status Led feature&lt;/li&gt;&#10;&lt;li&gt;User Input lock/unlock feature&lt;/li&gt;&#10;&lt;li&gt;Multiple playmodes [Album, Random, SaveProgress, OneTrack] available per Nfc Tag&lt;/li&gt;&#10;&lt;li&gt;Voice menus for linking or deletion of Nfc Tags&lt;/li&gt;&#10;&lt;li&gt;Optimized for battery applications (e.g. powerbank) using sleep states&lt;/li&gt;&#10;&lt;li&gt;Low power consumption @5V: ~40mA in idle, ~75mA playing medium volume&lt;/li&gt;&#10;&lt;li&gt;Config file for init volume, lullabye timer etc.&lt;/li&gt;&#10;&lt;li&gt;Auto-recovery from stuck prompts&lt;/li&gt;&#10;&lt;li&gt;Voice prompts for most common state errors&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="features-not-incliuded"&gt;Features not incliuded&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;No powerbank state of charge detection&lt;/li&gt;&#10;&lt;li&gt;No config settings menu (init volume, lullabye timer duration, standby duration etc.)&lt;/li&gt;&#10;&lt;li&gt;&amp;hellip;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="documentation"&gt;Documentation&lt;/h2&gt;&#10;&lt;p&gt;The Project is build for the Arduino Framework - tested on an Arduino nano board - using the &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;The following sections show the design.&lt;/p&gt;&#10;&lt;h3 id="project-module-overview"&gt;Project Module overview&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;Folder name in &lt;code&gt;/lib&lt;/code&gt;&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Purpose&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;minimalistic close-to-Hardware implementations, not unit-testable&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 abstraction to enable portability and testing&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;System configuration parameters&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;Playlist and playmode business logic&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;System messages and 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 control (Status, Folder, Prompts, Advertisements)&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 control (Status, Read, Write, Delete)&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;Control Status Led and Keep Alive based on system state&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;Main 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;Button or Encoder input handling&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;Timers, Led Control, Pin control&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;Link / Delete / Config menu business logic&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="external-libraries"&gt;External Libraries&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;Auto-install through Platformio on initial build through Library Dependency Finder.&lt;/p&gt;&#10;&lt;h3 id="class-diagrams"&gt;Class Diagrams&lt;/h3&gt;&#10;&lt;p&gt;Class diagrams would have been too much non-automated work. Take this instead, please.&#10;&lt;a href="https://raw.githubusercontent.com/Schallbert/Tonuino_alternativeFirmware/master/docs/ProjectModulesOverview.png" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Software module overview&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&#10;It should give an accurate understanding of how the software module are interacting and what APIs the modules are offering.&lt;/p&gt;&#10;&lt;h2 id="get-started"&gt;Get started!&lt;/h2&gt;&#10;&lt;p&gt;It&amp;rsquo;s DIY time!&#10;Clone my &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; to get started with. Read the &lt;code&gt;README.md&lt;/code&gt; document. Build it with PlatformIO.&#10;Below you can find what you need to get started with your unique speaker design.&lt;/p&gt;&#10;&lt;h2 id="testing"&gt;Testing&lt;/h2&gt;&#10;&lt;h3 id="unit-tests"&gt;Unit tests&lt;/h3&gt;&#10;&lt;p&gt;Unit tests are written with the gtest C++ unit test framework. The reside in the &lt;code&gt;test/desktop&lt;/code&gt; folder. Note that &lt;em&gt;googletest&lt;/em&gt; will require &lt;code&gt;gcc&lt;/code&gt; with some libraries to be installed, how to can be found &lt;a href="https://blog.schallbert.de/en/projects/platformio_gtestgmock/"&gt;in one of my other projects&lt;/a&gt;.&#10;Once these prerequisites are complete and a simple &lt;code&gt;ASSERT_TRUE(false);&lt;/code&gt; test fails as expected using the gtest environment, the project&amp;rsquo;s Unit Tests can be built and run using &lt;code&gt;pio test -e desktop -f desktop&lt;/code&gt;, &lt;a href="https://blog.schallbert.de/en/projects/platformio_gtestgmock/#writing-tests"&gt;detailed out here&lt;/a&gt;, in the PlatformIO CLI (terminal).&lt;/p&gt;&#10;&lt;h3 id="acceptance-tests"&gt;Acceptance tests&lt;/h3&gt;&#10;&lt;p&gt;Although these tests could be automated, it is much easier to just perform those tests by hand after you programmed your µC and put all electronic components together.&#10;Each line in the tables below is a test case on its own. The test suites (Heading name) do have a &lt;strong&gt;System Pre:&lt;/strong&gt; property, that needs to be re-established prior to executing the individual test cases. If the expectation clause is met, the test is a &lt;strong&gt;PASS&lt;/strong&gt;.&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 prompty 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="material-list"&gt;Material list&lt;/h3&gt;&#10;&lt;p&gt;Minimal discrete parts approach&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;Amount&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Item&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Purpose&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 (e.g. Nano)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Brains&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;Mouth&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 Card&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Memory&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 Reader&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;Bistable Relay 5V, e.g. HFE20&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Coffee - no sleep&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;Diodes e.g. 1n4007&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;vene valve&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 e.g. BC327&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Coffee maker switch&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;Resistors 1k&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;blood-stream conditioner&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;Resistor 220&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;dim blink strength&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, color of choice&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;blink&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 supply&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Food&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;push buttons, e.g. cherry MX keys&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;pressure sensors&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;speaker, e.g.5W@4Ohms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Lungs&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 plug&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Straw&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;housing&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Body&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="additional-coponents-and-tips"&gt;Additional coponents and tips&lt;/h3&gt;&#10;&lt;p&gt;Jumper wires, PCB, Sockets, and expendable materials as you deem fit&#10;Same as for the &amp;ldquo;original&amp;rdquo; project; Alternatively buy one rotary encoder instead of push buttons and configure the project to use it.&#10;As the system is optimized for battery usage, take a bi-stable relay or a JFET transistor (with low gate voltage and low Source-Drain voltage drop) for keepAlive functionality mitigating additional current add through always-on relay coils. A cheap powerbank will last for many hours. Alternatively, e.g. 3xAA batteries can be used - but the Dfmini will really make some noise if not enough current can be supplied due to running resets (and may even get bricked through current surges, so beware).&lt;/p&gt;&#10;&lt;h3 id="schematics"&gt;Schematics&lt;/h3&gt;&#10;&lt;figure class="media-frame media-frame--center"&gt;&#10; &lt;img src="https://raw.githubusercontent.com/Schallbert/Tonuino_alternativeFirmware/master/docs/CircuitDiagram.jpg" alt="Circuit Diagram"&gt;&lt;/figure&gt;&#10;</description></item></channel></rss>