<?xml version="1.0" encoding="utf-8"?> <feed xmlns="http://www.w3.org/2005/Atom"> <id>https://lerks.blog/feed.xml</id> <title>Lerks Blog</title> <subtitle>Tech, music, and stuff...</subtitle> <updated>2026-03-28T15:59:39+00:00</updated> <link href="https://lerks.blog/feed.xml" rel="self" type="application/atom+xml"/> <link href="https://lerks.blog/" rel="alternate" type="text/html"/><entry> <title>QR Thing for iOS/macOS</title> <id>https://lerks.blog/p/qr</id> <updated>2026-02-14T15:45:00+00:00</updated> <link href="https://lerks.blog/p/qr" rel="alternate" type="text/html" title="QR Thing for iOS/macOS"/><summary type="html"> <![CDATA[<p>I made a QR-Code scanner and creator app for iOS and macOS, you can scan QR Codes, open and/or copy their contents, and create new ones where you can also set their colors and shapes. As always the app is free and without spyware or ads.</p>]]> </summary><content type="html" xml:base="https://lerks.blog/"> <![CDATA[<p>I made a QR-Code scanner and creator app for iOS and macOS, you can scan QR Codes, open and/or copy their contents, and create new ones where you can also set their colors and shapes. As always the app is free and without spyware or ads.</p> <p>The main motivation for this was that I made a video where I embedded a QR code generated with a “trustworthy” QR creator. It turned out that the URL they put into the QR code pointed to their domain and they tried to extort a premium subscription from me by making the code no longer work as intended.</p> <p>So I used the anger about this obvious scam and that I was dumb enough to fall into their trap to write an app that does what I need while the new video was rendering.</p> <h2 id="features">Features</h2> <p>The app features are listed below:</p> <h3 id="scan-qr-codes">Scan QR Codes</h3> <p>Of course the app is able to scan QR codes, you can either open URLs that are contained in the QR or copy their value to use in other apps.</p> <h3 id="create-qr-codes">Create QR codes</h3> <p>You can use the app to create QR codes too, you can set the content type, content value, foreground and background colors, the pixel shapes, and export the result as PNG or SVG.</p> <h4 id="custom-shapes">Custom shapes</h4> <p>You can customize the shape of the QR code elements, for example you can make all pixels be circles instead of squares.</p> <h4 id="custom-colors-added-in-v120">Custom colors (Added in v1.2.0)</h4> <p>In addition to being able to change the overall foreground and background color you can change the colors of the different QR code elements.</p> <h4 id="custom-logos-added-in-v120">Custom logos (Added in v1.2.0)</h4> <p>You can add a custom logo from your photos to the QR code.</p> <h4 id="qr-code-validator-added-in-v130">QR Code Validator (Added in v1.3.0)</h4> <p>The app will now warn you if the generated QR Code might not be scannable or might be decoded in a different way than intended.</p> <h4 id="single-shape-mode-for-markers-added-in-v130">“Single-Shape” Mode for Markers (Added in v1.3.0)</h4> <p>You can now select “single-shape” mode for the QR marker elements, for example the inner QR marker can either be made up of (for example) circles or be a single (large) circle; Same with the outer marker, it can be made up of (for example) rounded rectangles or be a large rounded rectangle surrounding the inner marker.</p> <h2 id="planned-updates">Planned Updates</h2> <p>If you want to help with testing the new features listed below, you can join the <a href="https://lrk.lol/qr-beta">TestFlight Beta</a>.</p> <ul class="task-list"> <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Ability to set output size for PNG export</li> <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><del>Ability to scan and select between multiple codes</del> (added in 1.0.0)</li> <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />More scanner actions <ul class="task-list"> <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Import contacts from vCard codes</li> <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Save/Connect to WiFi from WiFi Codes</li> </ul> </li> </ul> <h2 id="download-link">Download Link</h2> <p>The app is completely free to use, without tracking or ads, and can be downloaded on the AppStore.</p> <p>I hope this app is useful, if not don’t hesitate to <a href="https://support.lrk.sh/qr">tell me the ways I can improve it</a>.</p> <p><a href="https://apps.apple.com/us/app/qr-code-scanner-creator/id6759196505?itscg=30200&amp;itsct=apps_box_badge&amp;mttnsubad=6759196505"> <img src="https://toolbox.marketingtools.apple.com/api/v2/badges/download-on-the-app-store/black/en-us?releaseDate=1747180800" alt="Download on the App Store" style="width: 245px; height: 82px; vertical-align: middle; object-fit: contain;" /> </a></p> ]]> </content> <author> <name>Lerk</name> </author><category term="ios"/><category term="macos"/><category term="app"/><category term="qr"/><category term="tools"/></entry><entry> <title>Wading through the Slop</title> <id>https://lerks.blog/p/slop</id> <updated>2026-02-08T17:15:00+00:00</updated> <link href="https://lerks.blog/p/slop" rel="alternate" type="text/html" title="Wading through the Slop"/><summary type="html"> <![CDATA[<p>I am sorry to report that I tried using Cursor (not on this blog or in a published project, but a Godot free time project). I canceled it again, the main reason is same as with ChatGPT I felt like what I got out of it was not something I made myself.</p>]]> </summary><content type="html" xml:base="https://lerks.blog/"> <![CDATA[<p>I am sorry to report that I tried using Cursor (not on this blog or in a published project, but a Godot free time project). I canceled it again, the main reason is same as with ChatGPT I felt like what I got out of it was not something I made myself.</p> <p>Plus I get headaches as soon as I feel another endless loop of slop hallucinations appear, sometimes even just watching the thinking process is enough to make me restart the session and try refining the prompt, because it’s hard enough to read through the 80% blurb to get the 20% information you actually need from any useful response. At that point I could almost search the web myself, but of course this is also not really possible anymore, but I will get to that.</p> <p>I used to view the code I built like an art project, now the “craft” is reduced to arguing with a machine. For example, I feel no personal connection to that Godot project besides the fact I wasted money for the subscription. I’d never say that about something I really made myself.</p> <p>And it’s not just the personal connection, because the solutions to problems I had the AI research for me didn’t stick in my brain when I needed them again later. <a href="https://www.media.mit.edu/publications/your-brain-on-chatgpt/">There is a study showing similar results</a>.</p> <p>Another thing that went bad is the joy of searching for solutions. Before AI turned search engines (or rather the results) into goop, researching a problem felt like conversing with ghosts of past legends to learn from their mistakes, now most of the top results are either ads, llm halluciantions, afiliate link spam, or any combination of those things. And while the engineers that would be in the position to fix the results are apparently busy <abbr title="building the torment nexus">doing something else</abbr>, it’s probably also an endless burn pit resources to keep fighting this.</p> <p>Because of that, the <em>best option to search the web for programming problems is currently Bing(Chat)</em> (I wish I was kidding) and the closest thing I get to joy when searching for solutions is the fake joy and overconfidence the language model has been forced to adapt. That is of course until the next “Here’s why that finally works <abbr title="😩">✨</abbr>” slop response wastes not only another hour of my time but also <a href="https://www.lincolninst.edu/publications/land-lines-magazine/articles/land-water-impacts-data-centers/">the</a> <a href="https://www.theguardian.com/environment/2026/jan/29/gas-power-ai-climate">worlds</a> <a href="https://newsletter.semianalysis.com/p/how-ai-labs-are-solving-the-power">finite resources</a>. And those resources as well as the thermal/pollution capacity of our biosphere and the scales we can shrink technology to <em>don’t scale infinitely</em>, so while you can of course <a href="https://www.wreflection.com/p/ai-dial-up-era">claim</a> this is like Dial-Up internet, it doesn’t make much sense in my opinion.</p> <p>Don’t get me wrong, I like computers, I LOVE to be able to bathe in the sounds of a full and busy server room, but I hate how all this is wasted for SLOP. Plain and simple. The level of usefulness is FAR below what it <a href="https://pcpartpicker.com/trends/price/internal-hard-drive/">costs</a> (<a href="https://pcpartpicker.com/trends/price/video-card/">us</a> <a href="https://pcpartpicker.com/trends/price/memory/">all</a>), even if the product itself is “free of charge”.</p> <p>All those cycles wasted…</p> <p>At least I am not the only one having these kind of feelings:</p> <ul> <li><a href="https://nolanlawson.com/2026/02/07/we-mourn-our-craft/">“We mourn our craft” - nolanlawson.com</a> <ul> <li>I’m not the target audience, but I have a similar fear for the future. I don’t think I’d be laughing though…</li> </ul> </li> <li><a href="https://ezhik.jp/ai-slop-terrifies-me/">“(AI) Slop Terrifies Me” - ezhik.jp</a> <ul> <li>After reading this I’m most terrified that the problem is not slop itself, but slop overload…</li> </ul> </li> <li><a href="https://abhinavomprakash.com/posts/i-am-happier-writing-code-by-hand/">“I Am Happier Writing Code by Hand” - abhinavomprakash.com</a> <ul> <li>The approach explained in the end with copying only the relevant code from the prompt area is similar to how I used BingChat and ChatGPT.</li> </ul> </li> <li><a href="https://siddhantkhare.com/writing/ai-fatigue-is-real">“AI fatigue is real and nobody talks about it” - siddhantkhare.com</a> <ul> <li>Made by an AI engineer, I couldn’t read most paragraphs to their end because it felt like AI, does this even count?</li> </ul> </li> </ul> <p>I’m not sure how to get back the ability to search for solutions like back in the day, I’m currently exploring kiwix and yacy in that regard, but I fear that unleashing LLM technology was a one-way action, and now searching the web can never be properly/productively done by a single human ever again…</p> ]]> </content> <author> <name>Lerk</name> </author><category term="rant"/><category term="slop"/><category term="software"/></entry><entry> <title>How to use TPM-backed SSH keys on macOS</title> <id>https://lerks.blog/p/macos-tpm-ssh</id> <updated>2025-12-31T00:00:00+00:00</updated> <link href="https://lerks.blog/p/macos-tpm-ssh" rel="alternate" type="text/html" title="How to use TPM-backed SSH keys on macOS"/><summary type="html"> <![CDATA[<p>I recently had to reinstall macOS (because the installation had ~250GB of “System Data” I am unable to purge, even as root) and used this opportunity to switch my SSH key setup on that device to be completely TPM-backed…</p>]]> </summary><content type="html" xml:base="https://lerks.blog/"> <![CDATA[<p>I recently had to reinstall macOS (because the installation had ~250GB of “System Data” I am unable to purge, even as root) and used this opportunity to switch my SSH key setup on that device to be completely TPM-backed…</p> <p>Welcome to the final post of 2025; Happy New Year!</p> <p>In this post I will list the necessary steps and recommend some configuration options that might be helpful if you happen to be in a similar situation, or are considering, or currently in the process of switching your SSH keys to be TPM-backed.</p> <p>One of the main benefits I hope to get from this change is peace of mind, as the private key will be kept in the TPM and can’t be (that easily) stolen by for example malware running as my user account (which would normally have read access to the private key). Additionally I hope to get more quality of life from using biometric authentication instead of passphrases when authenticating.</p> <h2 id="managing-tpm-keys">Managing TPM keys</h2> <p>This section describes the methods to manage private keys saved in the TPM.</p> <h3 id="generating-a-private-key">Generating a private key</h3> <p>The private key will be created using the built-in tool <code class="language-plaintext highlighter-rouge">sc_auth</code> (you can view its manual page using <code class="language-plaintext highlighter-rouge">man sc_auth</code>):</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sc_auth create-ctk-identity <span class="nt">-l</span> <span class="s2">"&lt;LABEL&gt;"</span> <span class="nt">-k</span> p-256-ne <span class="nt">-t</span> bio <span class="nt">-N</span> <span class="s2">"&lt;LABEL&gt;"</span> <span class="nt">-E</span> <span class="s2">"&lt;EMAIL&gt;"</span>
</code></pre></div></div> <p>You will have to replace:</p> <ul> <li><code class="language-plaintext highlighter-rouge">&lt;LABEL&gt;</code> with a label of your choice (I use <code class="language-plaintext highlighter-rouge">user@host</code> for ssh keys)</li> <li><code class="language-plaintext highlighter-rouge">&lt;EMAIL&gt;</code> with an email address representing the key (it’s optional, see the explanation of the parameters below)</li> </ul> <p>Here’s what the parameters mean:</p> <ul> <li><code class="language-plaintext highlighter-rouge">-l</code> is the key label for the list of keys saved in the TPM</li> <li><code class="language-plaintext highlighter-rouge">-k</code> is the key size, please note that only <code class="language-plaintext highlighter-rouge">256</code> and <code class="language-plaintext highlighter-rouge">384</code> are supported, <strong>but 384 is not supported by ssh</strong>; the <code class="language-plaintext highlighter-rouge">-ne</code> suffix means <em>non-exportable</em>, if you choose the key size without that suffix the private key can be exported from the TPM</li> <li><code class="language-plaintext highlighter-rouge">-t</code> is the private key protection, allowed values are <code class="language-plaintext highlighter-rouge">bio</code> for biometric protection, or <code class="language-plaintext highlighter-rouge">none</code></li> <li><code class="language-plaintext highlighter-rouge">-N</code> is the common name attribute of the key, this is not used by ssh and only added for decorative purposes (similar to the label, so I used the same value)</li> <li><code class="language-plaintext highlighter-rouge">-E</code> is the email address attribute of the key, same as the common name it’s not used by ssh and only set for decorative purposes</li> </ul> <h3 id="listing-and-removing-keys">Listing and removing keys</h3> <p>To list keys saved in the TPM you can use:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sc_auth list-ctk-identities
</code></pre></div></div> <p>To delete a certain key, you can note its ID in the output of the command above and run:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sc_auth delete-ctk-identity <span class="nt">-h</span> &lt;ID&gt;
</code></pre></div></div> <p>You have to replace <code class="language-plaintext highlighter-rouge">&lt;ID&gt;</code> with the ID of the key you want to delete.</p> <h3 id="exporting-the-keys-for-ssh">Exporting the keys for SSH</h3> <p>To export the public key and a reference to the credential stored in the TPM, you can run this command:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cd ~/.ssh
ssh-keygen -w /usr/lib/ssh-keychain.dylib -K -N ""
</code></pre></div></div> <p>This will export the keys saved in your TPM into the current folder (which is <code class="language-plaintext highlighter-rouge">~/.ssh</code> after the <code class="language-plaintext highlighter-rouge">cd</code> command).</p> <p>Note that if the <code class="language-plaintext highlighter-rouge">~/.ssh</code> folder doesn’t exist, the best way to create it is letting SSH handle it by running <code class="language-plaintext highlighter-rouge">ssh-keygen -t rsa -b 1024 -C temp</code>, then spam enter until the key is generated, followed by <code class="language-plaintext highlighter-rouge">rm ~/.ssh/id_rsa*</code> to delete the key and be left only with a working ssh dir.</p> <p>Here’s what the parameters mean:</p> <ul> <li><code class="language-plaintext highlighter-rouge">-w</code> sets the keychain library to use (the path points to a system library used to communicate with the TPM)</li> <li><code class="language-plaintext highlighter-rouge">-K</code> tells <code class="language-plaintext highlighter-rouge">ssh-keygen</code> to load keys from “the first connected FIDO authenticator” (which is the TPM in most cases)</li> <li><code class="language-plaintext highlighter-rouge">-N</code> sets the key passphrase, we use an empty string since the user presence is confirmed using biometrics (you could additionally use a passphrase)</li> </ul> <h2 id="using-the-key-by-default-for-all-hosts">Using the key by default for all hosts</h2> <p>Since I want to use this key as default for all hosts, the easiest way to set this is using my <code class="language-plaintext highlighter-rouge">~/.ssh/config</code> file, which is also taken into account when using <code class="language-plaintext highlighter-rouge">git</code>, <code class="language-plaintext highlighter-rouge">rsync</code>, etc. I added this part:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Host *
	IdentityFile ~/.ssh/id_ecdsa_sk_rk
	IdentitiesOnly yes
	SecurityKeyProvider /usr/lib/ssh-keychain.dylib
</code></pre></div></div> <p>What we’re telling ssh here is that for any host (<code class="language-plaintext highlighter-rouge">*</code>), we want to use the TPM library, the TPM key and that we don’t want to use any other key file than the one specified.</p> <p>You can (and probably need to) change this to match your setup.</p> <h2 id="deploying-the-public-key-on-hosts">Deploying the public key on hosts</h2> <p>Deploying the public key on your hosts should work as normal (you can use a multitude of methods).</p> <p>The one thing I thought was weird is that in the exported key the key comment is just <code class="language-plaintext highlighter-rouge">ssh:</code> and I was unable to get it set to something else using <code class="language-plaintext highlighter-rouge">sc_auth</code>. Fortunately you can simply change the comment of public keys in your hosts, it will not be validated when you try to connect.</p> <h2 id="connecting-to-hosts">Connecting to hosts</h2> <p>Now when you run <code class="language-plaintext highlighter-rouge">ssh user@myhost</code> with a valid user against one of your machines, you should be prompted to provide biometric authentication, similar to this:</p> <p><img height="320" style="height: 320px;" src="/assets/images/macos-tpm-auth.png" alt="screenshot of the macOS biometric auth dialog prompting to authenticate an ssh session" /></p> <p>And two new log messages in the SSH output:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Confirm user presence for key ECDSA-SK SHA256:&lt;HASH&gt;    # when the dialog appears
User presence confirmed                                 # after successful confirmation
</code></pre></div></div> <p>I think this is an improvement compared to using passphrases (I don’t like to use <code class="language-plaintext highlighter-rouge">ssh-agent</code>, and passphrases get annoying over time), I’m not sure about how much of a hassle this will become once all my keys are hardware-backed and all hardware goes down, but once I get to that point I will surely®️™️ have a proper backup strategy for this case.</p> <p>Regarding the experience of authenticating, I think there should be a better icon and description used in the authentication dialog, which better indicates where the authentication is coming from and what it’s for, apart from that I’m happy with how this currently works.</p> <hr /> <p>I hope this post was useful. If you have any comments, suggestions, or found errors please <a href="https://support.lrk.sh/blog">let me know</a>!</p> ]]> </content> <author> <name>Lerk</name> </author><category term="how-to"/><category term="macos"/><category term="software"/><category term="ssh"/><category term="tpm"/><category term="security"/></entry><entry> <title>How to solve Windows PCIe network latency</title> <id>https://lerks.blog/p/windows-pci-network-latency</id> <updated>2025-11-11T20:00:00+00:00</updated> <link href="https://lerks.blog/p/windows-pci-network-latency" rel="alternate" type="text/html" title="How to solve Windows PCIe network latency"/><summary type="html"> <![CDATA[<p>When I installed a new PCIe network card into my gaming PC I was surprised by suddenly lagging mouse input (via network, it’s a headless machine)…</p>]]> </summary><content type="html" xml:base="https://lerks.blog/"> <![CDATA[<p>When I installed a new PCIe network card into my gaming PC I was surprised by suddenly lagging mouse input (via network, it’s a headless machine)…</p> <p>It turns out the driver for the network card had a setting for throttling interrupts to the CPU, which was enabled by default. This reduces resource usage, but in my case it introduces problems, because incoming packets (for example me trying to move the mouse in a game) are not immediately sent to the CPU for processing, but rather collected over some amount of time or number of packets.</p> <p>The way to disable it is:</p> <ol> <li>Open device manager</li> <li>Open the Properties window of the network card</li> <li>Select the “Advanced” tab</li> <li>Set “Interrupt throttling” to “disabled”</li> <li>Click “Apply”</li> </ol> <p>To be sure I rebooted the PC, but I immediately noticed a less laggy input when applying the setting.</p> <p>If this applies to you, maybe your driver has a similar setting which is enabled by default.</p> <p>The other way around might apply as well. If you notice FPS drop or similar issues in a busy network (and are not streaming your input via network), enabling this might give other parts of the system more breathing room. The driver for the card I use also had a setting for changing the “throttling amount”, but only for arbitrary values like “high”, “medium”, and “low”, instead of allowing me to set a time or a number of incoming packets.</p> <hr /> <p>I hope this post was useful. If you have any suggestions or found errors please <a href="https://support.lrk.sh/blog">let me know</a>!</p> ]]> </content> <author> <name>Lerk</name> </author><category term="how-to"/><category term="windows"/><category term="hardware"/><category term="drivers"/></entry><entry> <title>Aurora</title> <id>https://kratzen-und-fauchen.com/releases/5063858010480</id> <updated>2025-11-11T00:00:00+00:00</updated> <link href="https://kratzen-und-fauchen.com/releases/5063858010480" rel="alternate" type="text/html" title="Aurora"/><content type="html" xml:base="https://lerks.blog/"> <![CDATA[<p>My first ever EP, “Aurora” contains four tracks I made while being inspired by the beauty and physics of nothern lights.</p>]]> </content> <author> <name>Lerk</name> </author><category term="music"/><category term="techno"/><category term="electronica"/></entry><entry> <title>Slop Has Reached GitLab</title> <id>https://lerks.blog/p/slop-has-reached-gitlab</id> <updated>2025-10-15T20:20:00+00:00</updated> <link href="https://lerks.blog/p/slop-has-reached-gitlab" rel="alternate" type="text/html" title="Slop Has Reached GitLab"/><summary type="html"> <![CDATA[<p>Slopware is now the default experience when you try to create a README file in an empty project using the GitLab Web UI.</p>]]> </summary><content type="html" xml:base="https://lerks.blog/"> <![CDATA[<p>Slopware is now the default experience when you try to create a README file in an empty project using the GitLab Web UI.</p> <p>Instead of being able to quickly create a new project with the summary of the idea in the README using the GitLab Web UI, I now end up having to use some slop Microsoft product (the “GitLab WebIDE” is just a broken web-port of VSCode) that can’t even commit a simple <code class="language-plaintext highlighter-rouge">README.md</code> into an empty repository: <img src="/assets/images/slop-ide-error.png" alt="Screenshot of an error message saying &quot;Failed to commit changes. See the console for more information.&quot;" /></p> <p>Correct me if I’m wrong, but I expected the default editor on a site built around hosting code with Git would let you commit to a Git repo.</p> <p>Also, hey WebIDE, is this console with more information in the room with us right now? Because all the panels I can see are:</p> <ul> <li>Comments <ul> <li>Showing a “There are no comments in this workspace yet” placeholder text</li> </ul> </li> <li>Terminal <ul> <li>Showing a “The terminal is not available in the Web IDE” placeholder text</li> </ul> </li> <li>Output <ul> <li><em>Which is empty</em></li> </ul> </li> <li>Problems <ul> <li>Showing a “No problems have been detected in this workspace” placeholder text</li> </ul> </li> </ul> <p>And of course in <abbr title="Nothing beats the Windows 10 bug where you could drag images/logos embedded into Windows installer packages from the installer dialog onto the Desktop to save them, like everything is just a browser; Another great example are the zillions of admin centers you have to use just to complete a single task, which feature every single design language they have ever used, if you can even open them without getting an error.">good Microsoft fashion</abbr>, you can only copy <em>some</em> of the panel placeholder texts, who needs a consistent experience after all, just ask the AI to put more slop on top of it until you don’t notice it anymore.</p> <p>I’m sad (apart from the anger I feel because using a previously useful tool now resulted in me having to look at yet another ugly and broken Microsoft UI) to see another default option being slop, what is happening to software QA?</p> <p>And I wonder, same as when I see Microsoft employees holding presentations about their slop OS using MacBooks, if people really use the software they put out anymore.</p> <p><strong>UPDATE Nov 2025:</strong> You know what the world needs right now? - That’s right:</p> <p><a href="https://web.archive.org/web/20251119031709/https://antigravity.google/">Yet another slop VSCode fork, of course with AI integrated</a></p> <p>Imagine that, the company that (once) built the (once) best browser, the (once) best search engine, the (once) best <abbr title="When it not yet meant wasting a families yearly power budget just to generate slop like &quot;Ah, you're totally right!&quot; or &quot;This is why this finally works:&quot; (it doesn't)">AI*</abbr>, the (once) best mobile operating system is now playing catch-up in wasting resources with the other big multicolored dystocorp.</p> <p>VSCode is the new Chromium. Can’t wait for <abbr title="What I felt eminating from the product after I saw the first screenshot of the UI on the website…">“Antigravity”</abbr> to be mothballed in a year or so…</p> ]]> </content> <author> <name>Lerk</name> </author><category term="gitlab"/><category term="software"/><category term="microsoft"/><category term="slop"/><category term="rant"/></entry><entry> <title>How to install encrypted Debian on an M1 MacBook Pro</title> <id>https://lerks.blog/p/how-to-install-encrypted-debian-on-m1-macbook-pro</id> <updated>2025-10-04T21:44:00+00:00</updated> <link href="https://lerks.blog/p/how-to-install-encrypted-debian-on-m1-macbook-pro" rel="alternate" type="text/html" title="How to install encrypted Debian on an M1 MacBook Pro"/><summary type="html"> <![CDATA[<p>In this post I’ll describe how to install an encrypted Debian (13, “Trixie”) on an M1 MacBook Pro (others might work too).</p>]]> </summary><content type="html" xml:base="https://lerks.blog/"> <![CDATA[<p>In this post I’ll describe how to install an encrypted Debian (13, “Trixie”) on an M1 MacBook Pro (others might work too).</p> <p>I have tried the Asahi Linux Fedora distribution a while ago and was surprised at how well it worked, the Asahi Contributors did an awesome job! However, I am more familiar with Debian, so I wanted to use that. Disk encryption is also required because the device is a laptop and I’m not sure yet where I might take it tomorrow.</p> <h2 id="important">Important</h2> <ul> <li>First of all, ensure that there is enough free space on your macOS partition! <ul> <li>You will need enough space for your Debian installation (only you know how much you need), and a temporary second installation which will be used to encrypt the main one.</li> </ul> </li> <li>It’s important to install/create the temporary Debian install <strong><em>after</em></strong> the primary one, so the space used for the temporary installation will able to be reintegrated into the macOS partition.</li> <li>To make it easy to differentiate between the primary and temporary installation, I’d suggest using a different GUI in the temporary installation.</li> <li><strong>Doing things wrong can brick your device. You need a second Mac in order to reflash the one you modified in case you (for example) accidentally delete the macOS Recovery or something like that!</strong></li> <li><strong>Don’t run this on your primary machine unless you have backups and a way to recover (a second Mac)!</strong></li> </ul> <h2 id="sources">Sources</h2> <ul> <li>There is an <a href="https://wiki.debian.org/InstallingDebianOn/Apple/M1">Article about installing Debian on M1 Hardware in the Debian Wiki</a> which is very helpful for a basic overview of the installer and how it works.</li> <li>There is a very helpful <a href="https://blog.williamdes.eu/Infrastructure/tutorials/encrypt-an-existing-debian-system-with-luks/">post about encrypting an already existing Debian installation</a> on which this post is based, with additional modifications for M1 Macs/Asahi I found while going through it!</li> <li>The <a href="https://asahilinux.org/docs/sw/partitioning-cheatsheet/">Asahi Partitioning Cheat Sheet</a> might be helpful as well.</li> </ul> <h2 id="installation">Installation</h2> <p>Let’s start with the installation (all Linux commands are supposed to be run as root, all macOS commands as your primary macOS user unless otherwise noted)!</p> <h3 id="installing-the-primary-debian">Installing the primary Debian</h3> <p>In macOS, open the terminal and run <code class="language-plaintext highlighter-rouge">curl -L https://bananas.debian.net/install | sh</code>, follow the instructions the script gives and <strong>read everything at least twice before pressing enter</strong>.</p> <p>Initially, the script will only offer to shrink your macOS installation, select that, follow the instructions to set the desired partition sizes (without the temporary second installation for now, only the primary one), and after it is done you will be sent back to the partition overview where the free space will be shown alongside an additional option to install Debian into it. Start the installation, then wait until the script shows the instructions for the next steps.</p> <p>Read the instructions carefully.</p> <p>The script will have you reboot into the Startup Options then choose the newly installed Debian installation which will actually launch a macOS recovery where you have to authenticate three times using your macOS credentials (once for accessing the recovery, then once to disable SIP, and then once again along with your username to change the boot policy for the Debian installation), after which the install script will finally boot the new Debian installation. In there you could do some first-time setup tasks now, but I’d do most of that (except maybe updates and keyboard/locale settings) when the encryption is done.</p> <h4 id="preparing-for-the-encryption">Preparing for the encryption</h4> <p>There are some tasks that need to be done in order to prepare the installation for the encryption while it is booted:</p> <ol> <li>Install required packages: <code class="language-plaintext highlighter-rouge">apt install cryptsetup cryptsetup-initramfs</code></li> <li>Add the line <code class="language-plaintext highlighter-rouge">GRUB_ENABLE_CRYPTODISK=y</code> to <code class="language-plaintext highlighter-rouge">/etc/default/grub</code></li> <li>Run <code class="language-plaintext highlighter-rouge">update-initramfs -u -k all &amp;&amp; update-grub</code> to update the initramfs and GRUB</li> </ol> <p>After this is done, shut down the system and wait for the device to be fully off, then press and hold the power button to go into Startup Options, then select the macOS partition to boot back into macOS.</p> <h3 id="installing-the-temporary-debian">Installing the temporary Debian</h3> <p>If you (like me) selected to use all the available space for the Debian install, you might need to clean up more space by deleting more stuff (maybe clean the bin for once). Otherwise (if you read the guide and reserved the space beforehand) it’s time to run the installer (<code class="language-plaintext highlighter-rouge">curl -L https://bananas.debian.net/install | sh</code>) again, to shrink the macOS partition once more, the temporary installation doesn’t need much space, I ended up using 30GB because it was available.</p> <p>After new free space is created, select to install Debian into the free space and <em>give the installation a different name, so you can easily separate the two</em>. Then it’s the same procedure as before, let the script run, then reboot, select the new temporary installation, authenticate in recovery in order to make it bootable, then let it reboot.</p> <p>This time it will boot the wrong Debian instance, which can be detected by the first time setup not appearing. In that case you have to select the temporary installation once again in Startup Options, after which the first time setup for the temporary installation should appear.</p> <h3 id="encrypting-the-primary-debian">Encrypting the primary Debian</h3> <p>In the temporary Debian, we can now encrypt the primary installation, but first of all it’s time to…</p> <h4 id="install-dependencies">Install dependencies</h4> <p>Make sure the connection to the internet (or your mirror) is working, and install the required packages: <code class="language-plaintext highlighter-rouge">apt install cryptsetup</code>.</p> <h4 id="find-partitions">Find partitions</h4> <p>After that, run <code class="language-plaintext highlighter-rouge">fdisk -l</code>, and search through the output, we will need three partition paths for later from it (it’s easiest to do that in order):</p> <ol> <li>The primary Debian installation root partition <ul> <li>This will probably be the largest “Linux filesystem” partition in the output</li> <li>It might have a slightly smaller size than what you set in the installer</li> </ul> </li> <li>The boot partition of the primary Debian installation <ul> <li>This will be the smaller “Linux filesystem” partition right before the primary partition</li> </ul> </li> <li>The EFI partition of the primary Debian installation <ul> <li>This will be the “EFI System” partition right before or after the boot partition of the primary Debian installation</li> </ul> </li> </ol> <p>In my case, <code class="language-plaintext highlighter-rouge">p10</code> was the primary Debian, <code class="language-plaintext highlighter-rouge">p8</code> was the EFI partition and <code class="language-plaintext highlighter-rouge">p9</code> the boot partition, but this might differ if you have customized your partition layout.</p> <p>Ensure that you picked the right partitions! If you accidentally update the wrong boot partition you will have to erase both Debian installations from macOS and start from the beginning again.</p> <h4 id="encrypt-the-partition">Encrypt the partition</h4> <p>Create a script file (for example <code class="language-plaintext highlighter-rouge">~/encrypt.sh</code>) and paste the following:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>

<span class="nb">set</span> <span class="nt">-euo</span> pipefail

<span class="c"># DISCLAIMER: USE AT YOUR OWN RISK AND MAKE BACKUPS</span>

<span class="c"># Based on instructions from:</span>
<span class="c"># https://blog.williamdes.eu/Infrastructure/tutorials/encrypt-an-existing-debian-system-with-luks/</span>
<span class="c"># Which is in turn based on instructions from:</span>
<span class="c"># https://wiki.archlinux.org/index.php/dm-crypt/Device_encryption#Encrypt_an_existing_unencrypted_filesystem</span>

<span class="c"># Save partition path as variable</span>
<span class="nv">DISK</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">1</span><span class="k">:-}</span><span class="s2">"</span>

<span class="c"># Check if path exists</span>
<span class="k">if</span> <span class="o">[</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$DISK</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
	</span><span class="nb">echo</span> <span class="s2">"Usage: </span><span class="nv">$0</span><span class="s2"> /dev/nvmeXnXpX"</span>
	<span class="nb">exit </span>1
<span class="k">fi</span>

<span class="c"># Run a filesystem check on the selected partition</span>
e2fsck <span class="nt">-f</span> <span class="s2">"</span><span class="nv">$DISK</span><span class="s2">"</span>

<span class="c"># Make the filesystem slightly smaller to make space for the LUKS header</span>
<span class="nv">BLOCK_SIZE</span><span class="o">=</span><span class="sb">`</span>dumpe2fs <span class="nt">-h</span> <span class="nv">$DISK</span> | <span class="nb">grep</span> <span class="s2">"Block size"</span> | <span class="nb">cut</span> <span class="nt">-d</span> <span class="s1">':'</span> <span class="nt">-f</span> 2 | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">' '</span><span class="sb">`</span>
<span class="nv">BLOCK_COUNT</span><span class="o">=</span><span class="sb">`</span>dumpe2fs <span class="nt">-h</span> <span class="nv">$DISK</span> | <span class="nb">grep</span> <span class="s2">"Block count"</span> | <span class="nb">cut</span> <span class="nt">-d</span> <span class="s1">':'</span> <span class="nt">-f</span> 2 | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">' '</span><span class="sb">`</span>
<span class="nv">SPACE_TO_FREE</span><span class="o">=</span><span class="k">$((</span><span class="m">1024</span> <span class="o">*</span> <span class="m">1024</span> <span class="o">*</span> <span class="m">32</span><span class="k">))</span> <span class="c"># 16MB should be enough, but add a safety margin</span>
<span class="nv">NEW_BLOCK_COUNT</span><span class="o">=</span><span class="k">$((</span><span class="nv">$BLOCK_COUNT</span> <span class="o">-</span> <span class="nv">$SPACE_TO_FREE</span> <span class="o">/</span> <span class="nv">$BLOCK_SIZE</span><span class="k">))</span>
resize2fs <span class="nt">-p</span> <span class="s2">"</span><span class="nv">$DISK</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$NEW_BLOCK_COUNT</span><span class="s2">"</span>

<span class="c"># Run the encryption process</span>
<span class="c"># MAN: https://man7.org/linux/man-pages/man8/cryptsetup-reencrypt.8.html</span>
cryptsetup reencrypt <span class="nt">--encrypt</span> <span class="nt">--reduce-device-size</span> 16M <span class="s2">"</span><span class="nv">$DISK</span><span class="s2">"</span>

<span class="c"># Resize the filesystem to fill up the remaining space (i.e. remove the safety margin from earlier)</span>
cryptsetup open <span class="s2">"</span><span class="nv">$DISK</span><span class="s2">"</span> recrypt
resize2fs /dev/mapper/recrypt
cryptsetup close recrypt
<span class="nb">echo</span> <span class="s2">"Done!"</span>
</code></pre></div></div> <p>Then save and close the file. This script will first make the primary partition a bit smaller to fit the LUKS header, then encrypt the partition (and prompt you to enter <code class="language-plaintext highlighter-rouge">YES</code> and the password you want to use to decrypt the partition), and after that prompt for the same password again in order to open the encrypted partition to resize it to its original (or almost original) size. After the file is created, make it executable by running <code class="language-plaintext highlighter-rouge">chmod +x ~/encrypt.sh</code>, and then run it:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~/encrypt.sh
</code></pre></div></div> <p>After everything is done, and <code class="language-plaintext highlighter-rouge">Done!</code> was logged to the terminal, the partition is encrypted, but there is still some work to do.</p> <h4 id="grub-compatibility">GRUB Compatibility</h4> <p>Unfortunately, GRUB doesn’t support the <code class="language-plaintext highlighter-rouge">argon2id</code> key derivation function (<a href="https://www.reddit.com/r/debian/comments/15w9c6e/cryptomount_invalid_passphrase_despite_being/">source 1</a>, <a href="https://unix.stackexchange.com/a/789998">source 2</a>) that cryptsetup uses by default, so the key has to be changed to PBKDF2 (replace <code class="language-plaintext highlighter-rouge">&lt;PARTITION_PATH&gt;</code> with the path of the primary (now encrypted) partition):</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cryptsetup luksConvertKey <span class="nt">--pbkdf</span> pbkdf2 &lt;PARTITION_PATH&gt;
</code></pre></div></div> <p>After that, it is a good idea to check if everything still works:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cryptsetup <span class="nt">--verbose</span> open <span class="nt">--test-passphrase</span> &lt;PARTITION_PATH&gt;
</code></pre></div></div> <h4 id="making-it-bootable">Making it bootable</h4> <p>The final step is to update the configuration files of the primary Debian installation so that it can successfully boot with the now very different setup.</p> <h5 id="chrooting-into-the-primary-install">Chrooting into the primary install</h5> <p>To do that we will mount the primary Debian installation into the currently running temporary one, and use the <code class="language-plaintext highlighter-rouge">chroot</code> tool in order to change some things before we can boot.</p> <p>As I did in my <a href="/p/how-to-securely-set-up-a-new-server">post about remotely setting up an encrypted Debian server</a>, I will use <code class="language-plaintext highlighter-rouge">cryptroot</code> as the encrypted root partition id, you might want to change it if your preferences differ.</p> <p>Replace <code class="language-plaintext highlighter-rouge">&lt;ROOT_PARTITION_PATH&gt;</code> with the path of the (encrypted) primary partition, <code class="language-plaintext highlighter-rouge">&lt;BOOT_PARTITION_PATH&gt;</code> with the path to the boot “Linux filesystem” partition of the primary installation, and <code class="language-plaintext highlighter-rouge">&lt;EFI_PARTITION_PATH&gt;</code> with the path to the EFI system partition of the primary installation.</p> <ol> <li>Unlock the encrypted partition: <ul> <li><code class="language-plaintext highlighter-rouge">cryptsetup luksOpen &lt;ROOT_PARTITION_PATH&gt; cryptroot</code></li> </ul> </li> <li>Mount the unlocked partition: <ul> <li><code class="language-plaintext highlighter-rouge">mount /dev/mapper/cryptroot /mnt</code></li> </ul> </li> <li>Mount the boot partition: <ul> <li><code class="language-plaintext highlighter-rouge">mount &lt;BOOT_PARTITION_PATH&gt; /mnt/boot</code></li> </ul> </li> <li>Mount the efi partition: <ul> <li><code class="language-plaintext highlighter-rouge">mount &lt;EFI_PARTITION_PATH&gt; /mnt/boot/efi</code></li> </ul> </li> <li>Mount some required things from the running temporary Debian: <ul> <li><code class="language-plaintext highlighter-rouge">mount --bind /dev /mnt/dev</code></li> <li><code class="language-plaintext highlighter-rouge">mount --bind /sys /mnt/sys</code></li> <li><code class="language-plaintext highlighter-rouge">mount --bind /proc /mnt/proc</code></li> <li><code class="language-plaintext highlighter-rouge">mkdir -p /mnt/run/udev</code></li> <li><code class="language-plaintext highlighter-rouge">mount --bind /run/udev /mnt/run/udev</code></li> </ul> </li> <li>Chroot into the primary installation: <ul> <li><code class="language-plaintext highlighter-rouge">LANG=C.UTF-8 chroot /mnt /bin/bash</code></li> </ul> </li> </ol> <p>It might appear that nothing has changed with the last command, but if there was no error, and you see a root prompt, you’ve successfully changed into the primary installation.</p> <h5 id="configuring-the-primary-system">Configuring the primary system</h5> <p>Next, run <code class="language-plaintext highlighter-rouge">blkid -o value -s UUID &lt;ROOT_PARTITION_PATH&gt;</code> and copy/remember/write down the UUID (we will refer to this as <code class="language-plaintext highlighter-rouge">&lt;ROOT_PARTITION_UUID&gt;</code>).</p> <ol> <li>Edit <code class="language-plaintext highlighter-rouge">/etc/crypttab</code> and add the following line: <ul> <li><code class="language-plaintext highlighter-rouge">cryptroot UUID=&lt;ROOT_PARTITION_UUID&gt; none luks</code></li> </ul> </li> <li>Edit <code class="language-plaintext highlighter-rouge">/etc/fstab</code> and change the line for the root device so that it uses <code class="language-plaintext highlighter-rouge">/dev/mapper/cryptroot</code> as device instead of what it currently uses (in my case a UUID).</li> <li>Edit <code class="language-plaintext highlighter-rouge">/etc/default/grub</code> and add the following into the <code class="language-plaintext highlighter-rouge">GRUB_CMDLINE_LINUX=""</code> variable content: <ul> <li><code class="language-plaintext highlighter-rouge">cryptdevice=UUID=&lt;ROOT_PARTITION_UUID&gt;:cryptroot root=/dev/mapper/cryptroot</code></li> </ul> </li> <li>Update the initramfs and GRUB: <ul> <li><code class="language-plaintext highlighter-rouge">update-initramfs -u -k all &amp;&amp; update-grub</code></li> </ul> </li> </ol> <p><em>It is at this point where the error I talked about earlier can happen, where you accidentally identified the boot and efi partitions of the temporary system and mounted them into the primary system, the primary system won’t be able to boot because it can’t handle decryption, and the temporary system won’t boot anymore wither since it then tries to but can’t find any encrypted drive!</em></p> <h5 id="finishing-up">Finishing up</h5> <p>Now it’s time to <code class="language-plaintext highlighter-rouge">exit</code> the chroot, and then unmount all the disks and reboot using:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>umount /mnt/boot/efi /mnt/boot /mnt/proc /mnt/sys /mnt/dev /mnt/run/udev <span class="c"># sometimes I need to run this twice</span>
umount /mnt <span class="c"># If this says "target is busy", run the previous umount again</span>
<span class="nb">sync
</span>shutdown <span class="nt">-h</span> now
</code></pre></div></div> <p>It’s important to use <code class="language-plaintext highlighter-rouge">shutdown -h now</code> because we don’t want to reboot the temporary installation, we want to turn the device off.</p> <h2 id="booting-the-encrypted-installation">Booting the encrypted installation</h2> <p>When the device is fully off, press and hold the power button to boot into Startup Options, then select the primary Debian installation. Same as usual, it should boot the Asahi boot stage, then uBoot, followed by GRUB which loads the kernel, then you should get asked to unlock the primary partition, and after that the system should boot just as usual.</p> <h3 id="troubleshooting">Troubleshooting</h3> <p>In case something doesn’t work, you can shut down the system, enter Startup Options to boot from the temporary Debian, then <a href="#configuring-the-primary-system">chroot into the primary system</a> in order to double-check your configuration files, maybe it’s just some wrong UUID or value in the files that need to be configured in order for the system to properly find the newly encrypted root partition. If neither the temporary nor the primary installations will boot, it’s best to boot back into macOS, and then deleting the Linux partitions again in order to start from scratch.</p> <h2 id="cleaning-up">Cleaning up</h2> <p>After the encrypted primary installation was confirmed to be working, the temporary installation can be deleted to reclaim the space back for macOS. In order to do that, boot back into macOS, then:</p> <ol> <li>Open a terminal</li> <li>Run <code class="language-plaintext highlighter-rouge">diskutil list</code> <ul> <li>You will get multiple “synthesized” disks as output, one of which will be the temp installation, and another the macOS installation</li> <li>The name of the temp disk (without the path, for example <code class="language-plaintext highlighter-rouge">disk5</code>) will be <code class="language-plaintext highlighter-rouge">&lt;TEMP_DISK_NAME&gt;</code> in the next step</li> <li>The name of the macOS disk (without the path, for example <code class="language-plaintext highlighter-rouge">disk0</code>) will be <code class="language-plaintext highlighter-rouge">&lt;MACOS_DISK_NAME&gt;</code> in the next step</li> </ul> </li> <li>Run <code class="language-plaintext highlighter-rouge">diskutil apfs deleteContainer &lt;TEMP_DISK_NAME&gt;</code> to delete the temp installation</li> <li>Run <code class="language-plaintext highlighter-rouge">diskutil apfs resizeContainer &lt;MACOS_DISK_NAME&gt; 0</code> to resize the macOS disk to use the free space from the temp installation</li> </ol> <p>The final step is to shut down the machine once again and boot into Startup Options, because right now the (deleted) temp installation is still the primary booting os in EFI. To change this, select the primary Debian installation in Startup Options and <em>hold down the alt key</em> while you select it so it says “Always Use” on the button. This way, next time you reboot the Mac, Debian will boot automatically. If you decide to skip this step the MacBook will run into a bootloop a few times (I think it was three reboots), then it will default to boot macOS (at least it did in my case).</p> <hr /> <p>I hope this post was useful. If you have any suggestions or found errors please <a href="https://support.lrk.sh/blog">let me know</a>!</p> ]]> </content> <author> <name>Lerk</name> </author><category term="debian"/><category term="apple"/><category term="macbook"/><category term="how-to"/></entry><entry> <title>What happened to GNOME?</title> <id>https://lerks.blog/p/gnome</id> <updated>2025-10-03T22:22:00+00:00</updated> <link href="https://lerks.blog/p/gnome" rel="alternate" type="text/html" title="What happened to GNOME?"/><summary type="html"> <![CDATA[<p>The last time I used GNOME (a few years ago I think it was Version 3) I was surprised at how ugly it had gotten and how much of an “improvement” this was praised as.</p>]]> </summary><content type="html" xml:base="https://lerks.blog/"> <![CDATA[<p>The last time I used GNOME (a few years ago I think it was Version 3) I was surprised at how ugly it had gotten and how much of an “improvement” this was praised as.</p> <p>Turns out the only thing that really has changed is that it’s now basically impossible to make it less ugly.</p> <p>So many years have gone by, and still the trackpad feels as weird/glitchy as I remember, the sounds are annoying and on full blast by default, everything is HUGE with large paddings everywhere and animated in a way that makes it feel slippery like oil on linoleum; The first instinct I have is to change settings because everything feels awful, and there are three applications to configure the preferences, one of which is basically <code class="language-plaintext highlighter-rouge">regedit.exe</code>, why? No idea.</p> <p>I wasted two hours in trying to get it to look at least a bit better, I only managed to change the title bar of the terminal application, everything else seems to use this new fixed design that is 99% padding (== wasted space) and only available in two weird colors, did I mention that you can’t even change the size of the dock (or maybe I didn’t look deep enough into the cursed config editor thing).</p> <p>The resolution scaling is completely useless as well; because of the design not being configurable, I tried using the scaling to make it bearable, but I would need something like 132% to make it perfect and of course I can only choose between 125% (too small to read) or 150% (boomer mode), with a dropdown.</p> <p>At least there are visible improvements. Lots of time seem to have been sunk in reimplementing all the things Apple keeps cramming into macOS for some weird reason (which made me check out going back to Linux in the first place, oh well) but without the (nowadays really lacking) love and care (aka. QA) that MADE macOS feel special.</p> <p>Sad to see the primary/first option listed in the Debian installer being this bad.</p> <p>It’s not (yet) the time of the Linux Desktop (imo).</p> ]]> </content> <author> <name>Lerk</name> </author><category term="debian"/><category term="software"/><category term="gnome"/><category term="rant"/></entry><entry> <title>Amplifier/Effects for iOS</title> <id>https://lerks.blog/p/amp</id> <updated>2025-09-21T18:20:00+00:00</updated> <link href="https://lerks.blog/p/amp" rel="alternate" type="text/html" title="Amplifier/Effects for iOS"/><summary type="html"> <![CDATA[<p>I made an amplifier and effects app for iOS which lets users amplify their voice or instruments using any connected input and output the resulting audio over any connected output.</p>]]> </summary><content type="html" xml:base="https://lerks.blog/"> <![CDATA[<p>I made an amplifier and effects app for iOS which lets users amplify their voice or instruments using any connected input and output the resulting audio over any connected output.</p> <p>The main motivation for this was a new cable I got. It has USB-C with a DAC on one end and a guitar jack on the other (after using it for a while I wouldn’t recommend this specific model as it generates lots of noise). Initially I used this cable to connect my guitar to my laptop and then use either Logic or MainStage to have a virtual amplifier, but then I had to think of the cute little Marshall MS-2 pocket amplifier I had as a child, and wanted to build an app with this (and more) functionality for iOS (which is possible now that the iPhone supports USB-C, and with that generic USB audio devices).</p> <h2 id="features">Features</h2> <p>The app features are listed below:</p> <h3 id="freely-choose-input-and-output">Freely choose input and output</h3> <p>You can change the input and output device you want to use, for example you could amplify your voice using the built-in microphone out to a bluetooth speaker (keep in mind that bluetooth will add latency) to make an announcement, or use a multi port adapter with an additional audio adapter to route the generated audio back to a mixer if you forgot to bring your amp or pedalboard to band practice.</p> <h3 id="dual-gain-stages">Dual Gain Stages</h3> <p>There is pre- and post-gain, which lets you adjust the signal level in exactly the way you want/need. Pre gain is applied right after the input, post gain right before the output.</p> <h3 id="output-meter">Output Meter</h3> <p><img src="/assets/images/amp-output.gif" alt="cropped screen recording of the output section showing me badly playing guitar in a way that creates a clipped signal" /></p> <p>The application shows a waveform of the current output signal, a level meter, and a clip indicator which flashes red if the output signal is above 100% level.</p> <p>Since v1.1.0 the update interval of the output meter can be configured in order to match your preference or requirements.</p> <h3 id="distortion-reverb-and-delay">Distortion, Reverb, and Delay</h3> <p>The app has configurable distortion, reverb, and delay plugins with built-in presets to choose from and properties to configure.</p> <h3 id="equalizer">Equalizer</h3> <p>There is also a 6-band EQ with additional settings for each equalizer band.</p> <h3 id="configurable-plugin-chain">Configurable Plugin Chain</h3> <p>You can enable/disable plugins, and change their order (distortion, reverb, delay, noise gate, eq) within the mixing chain.</p> <h3 id="added-in-v110-noise-gate">(Added in v1.1.0) Noise Gate</h3> <p>The Noise Gate plugin allows for muting the input if the level falls below a configurable threshold.</p> <h3 id="added-in-v110-metronome">(Added in v1.1.0) Metronome</h3> <p>Use the metronome to practice your beat keeping, it can be used even when no input is selected.</p> <h3 id="added-in-v110-tuner">(Added in v1.1.0) Tuner</h3> <p>The app features a tuner which can be used to tune instruments or measure frequencies.</p> <h2 id="planned-updates">Planned Updates</h2> <p>If you want to help with testing the new features listed below, you can join the <a href="https://lrk.lol/amp-beta">TestFlight Beta</a>.</p> <ul class="task-list"> <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Pitch Shift effect/plugin</li> <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Configurable EQ Frequency Bands <ul class="task-list"> <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Ability to add/remove bands</li> <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Ability to change EQ band center frequency</li> </ul> </li> </ul> <h2 id="download-link">Download Link</h2> <p>The app is completely free to use, without tracking or ads, and can be downloaded on the AppStore.</p> <p>I hope this app is useful, if not don’t hesitate to <a href="https://support.lrk.sh/amp">tell me the ways I can improve it</a>.</p> <p><a href="https://apps.apple.com/us/app/amplifier-effects/id6752813371?itscg=30200&amp;itsct=apps_box_badge&amp;mttnsubad=6745765114"> <img src="https://toolbox.marketingtools.apple.com/api/v2/badges/download-on-the-app-store/black/en-us?releaseDate=1747180800" alt="Download on the App Store" style="width: 245px; height: 82px; vertical-align: middle; object-fit: contain;" /> </a></p> ]]> </content> <author> <name>Lerk</name> </author><category term="ios"/><category term="app"/><category term="audio"/><category term="tools"/><category term="instruments"/><category term="amplifier"/><category term="effects"/></entry><entry> <title>Lightspeed</title> <id>https://kratzen-und-fauchen.com/releases/5063807086795</id> <updated>2025-09-08T00:00:00+00:00</updated> <link href="https://kratzen-und-fauchen.com/releases/5063807086795" rel="alternate" type="text/html" title="Lightspeed"/><content type="html" xml:base="https://lerks.blog/"> <![CDATA[<p>After completing “Aberration”, I was reminded of how much fun I have while making music, this album is the result of this realization.</p>]]> </content> <author> <name>Lerk</name> </author><category term="music"/><category term="techno"/><category term="electronica"/></entry><entry> <title>Aberration</title> <id>https://kratzen-und-fauchen.com/releases/5063778970932</id> <updated>2025-09-01T00:00:00+00:00</updated> <link href="https://kratzen-und-fauchen.com/releases/5063778970932" rel="alternate" type="text/html" title="Aberration"/><content type="html" xml:base="https://lerks.blog/"> <![CDATA[<p>After a long period of focusing on other things, a sudden, profound loss pushed me to create music again as a way to process everything. This album is dedicated to Yuri the cat!</p>]]> </content> <author> <name>Lerk</name> </author><category term="music"/><category term="techno"/><category term="electronica"/></entry><entry> <title>Don&apos;t wear your Apple Watch Series 10</title> <id>https://lerks.blog/p/apple-watch-series-10</id> <updated>2025-08-02T15:30:00+00:00</updated> <link href="https://lerks.blog/p/apple-watch-series-10" rel="alternate" type="text/html" title="Don&apos;t wear your Apple Watch Series 10"/><summary type="html"> <![CDATA[<p>The paint on the bottom chips off when worn and the support says it costs 370€ to replace it while the watch is still under “warranty”.</p>]]> </summary><content type="html" xml:base="https://lerks.blog/"> <![CDATA[<p>The paint on the bottom chips off when worn and the support says it costs 370€ to replace it while the watch is still under “warranty”.</p> <p><img height="250" style="height: 250px;" src="/assets/images/watch-paint.jpg" alt="photo of the chipped-off paint on the bottom of the watch" /></p> <p>I would expect that they test the paint they use for a <em>wearable</em> product for chipping when worn. But I guess that’s too much to ask for nowadays, what happened to QA…</p> <p><strong>Update 02/26</strong>: I stopped wearing the watch because the software got too annoying and suddenly realized that my lower arm doesn’t hurt anymore (I wasn’t aware it was hurting before), the watch mark is still there though, four days after last wearing it.</p> ]]> </content> <author> <name>Lerk</name> </author><category term="hardware"/><category term="apple"/><category term="rant"/></entry><entry> <title>Building a HomeKit Doorbell using a RaspberryPi Zero</title> <id>https://lerks.blog/p/rpi-homekit-doorbell</id> <updated>2025-07-12T09:03:00+00:00</updated> <link href="https://lerks.blog/p/rpi-homekit-doorbell" rel="alternate" type="text/html" title="Building a HomeKit Doorbell using a RaspberryPi Zero"/><summary type="html"> <![CDATA[<p>I have a super-annoying doorbell in my flat, and my cats hate it. Even when it’s on the lowest volume, they panic and hide as soon as it goes off…</p>]]> </summary><content type="html" xml:base="https://lerks.blog/"> <![CDATA[<p>I have a super-annoying doorbell in my flat, and my cats hate it. Even when it’s on the lowest volume, they panic and hide as soon as it goes off…</p> <h2 id="initial-plans">Initial Plans</h2> <p>Luckily, the doorbell is powered using a 9V battery and only two cables for the trigger are connected to the actual doorbell module on our wall, using screw terminal blocks.</p> <p>Judging from this, it should be possible to remove those cables and put them onto custom hardware which then does some magic to notify me using my phone.</p> <p>My first thought was to write an iOS app, which can receive push notifications from the hardware, and initially I planned to build this using an ESP32 (because I already have some of those) but it turns out this wasn’t a good plan for multiple reasons:</p> <ul> <li>The ESP32 is way too underpowered to send Apple push notifications which uses modern encryption with HTTP/2</li> <li>The iOS app I would build to receive notifications will either have to be published to the AppStore (publicly) or rebuilt every few days when the developer certificate expires</li> <li>The notification won’t arrive if there are problems with the internet connection, since the notification goes through Apple Servers</li> </ul> <p>So I decided to do more research, luckily I found <a href="https://github.com/homebridge/HAP-NodeJS">HAP-NodeJS</a> which is a HomeKit library for Node.js which is made by the Homebridge developers. This needs a Node.js environment though, so the ESP32 would not work.</p> <h2 id="setting-up-the-hardware">Setting up the hardware</h2> <p>The next best piece of hardware (that I know of) is a RaspberryPi Zero W which is running Raspbian (Debian).</p> <p>For a how-to on configuring (and securing) the operating system on the Pi, you can use the <a href="https://lerks.blog/p/how-to-securely-set-up-a-new-server#post-installation-steps">post installation steps of my Debian server setup post</a>, as well as the official <a href="https://www.raspberrypi.com/documentation/computers/configuration.html#raspi-config">documentation provided by RaspberryPi</a>.</p> <p>It’s important to ensure that you install the <code class="language-plaintext highlighter-rouge">pigpiod</code> package by running: <code class="language-plaintext highlighter-rouge">apt install pigpiod</code> as root on the Pi.</p> <h2 id="getting-nodejs">Getting Node.js</h2> <p>There was another pitfall though, the Raspberry Pi Zero W is 32-bit (armv6l) only. The current Node.js LTS (v22) is not built for this anymore, I tried cross compiling it myself using a Debian aarch64 Docker image on my MacBook but that didn’t work because some tools during build would call armv6l executables which of course don’t run on aarch64. Next, I tried emulating a Raspberry Pi using qemu in UTM, but that would only let me use the actual specs of the Raspberry Pi (single CPU core, 512MB RAM), which would run slower than the actual hardware. So I tried compiling Node.js 22 directly on the Pi Zero, but the Pi lost WiFi connection after two days of uninterrupted compilation and I had to restart it because it wouldn’t reconnect, after which I would have to start the process all over again.</p> <p>So in the end I settled with the latest available Node.js prebuilt for armv6l, which turns out to be <a href="https://unofficial-builds.nodejs.org/download/release/v20.19.3/">Node.js v20.19.3</a>.</p> <h2 id="building-the-service">Building the Service</h2> <p>Now that we have the Node.js environment, we can start developing the service which will run on the Pi.</p> <h3 id="packagejson">Package.json</h3> <p>I created an initial <code class="language-plaintext highlighter-rouge">package.json</code> with the data I wanted and added two dependencies:</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"dependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"@homebridge/hap-nodejs"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^2.0.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"pigpio-client"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^1.5.2"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div> <p>Here’s a short explanation of the dependencies:</p> <ul> <li><a href="https://github.com/homebridge/HAP-NodeJS">HAP-NodeJS</a> is the HomeKit library which lets us communicate with HomeKit</li> <li><a href="https://github.com/fivdi/onoff">onoff</a> is a GPIO library which lets us control the GPIO pins of the Pi Zero</li> </ul> <h3 id="mainjs">Main.js</h3> <p>Now it’s time to add the actual code (you have the luxury of being able t copy it, but remember to change the id and metadata values to fit your setup):</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span><span class="nx">Accessory</span><span class="p">,</span> <span class="nx">Categories</span><span class="p">,</span> <span class="nx">Characteristic</span><span class="p">,</span> <span class="nx">Service</span><span class="p">,</span> <span class="nx">uuid</span><span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@homebridge/hap-nodejs</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">pkg</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../package.json</span><span class="dl">'</span> <span class="kd">with</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">json</span><span class="dl">'</span> <span class="p">};</span>
<span class="k">import</span> <span class="nx">PigpioClient</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">pigpio-client</span><span class="dl">'</span><span class="p">;</span>

<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Initializing…</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">accessoryUuid</span> <span class="o">=</span> <span class="nx">uuid</span><span class="p">.</span><span class="nf">generate</span><span class="p">(</span><span class="dl">"</span><span class="s2">com.example.doorbell</span><span class="dl">"</span><span class="p">);</span> <span class="c1">// this should stay the same for the entire lifetime of the accessory</span>
<span class="kd">const</span> <span class="nx">accessory</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Accessory</span><span class="p">(</span><span class="dl">"</span><span class="s2">Doorbell</span><span class="dl">"</span><span class="p">,</span> <span class="nx">accessoryUuid</span><span class="p">);</span> <span class="c1">// the name can be anything you want, can also be changed later</span>


<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Registering doorbell service…</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">doorbellService</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Service</span><span class="p">.</span><span class="nc">Doorbell</span><span class="p">(</span><span class="dl">"</span><span class="s2">Doorbell</span><span class="dl">"</span><span class="p">);</span> <span class="c1">// the service name can be anything you want </span>
<span class="kd">const</span> <span class="nx">triggerCharacteristic</span> <span class="o">=</span> <span class="nx">doorbellService</span>
    <span class="p">.</span><span class="nf">getCharacteristic</span><span class="p">(</span><span class="nx">Characteristic</span><span class="p">.</span><span class="nx">ProgrammableSwitchEvent</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">setProps</span><span class="p">({</span>
        <span class="na">validValues</span><span class="p">:</span> <span class="p">[</span><span class="nx">Characteristic</span><span class="p">.</span><span class="nx">ProgrammableSwitchEvent</span><span class="p">.</span><span class="nx">SINGLE_PRESS</span><span class="p">]</span>
    <span class="p">});</span>
<span class="nx">accessory</span><span class="p">.</span><span class="nf">addService</span><span class="p">(</span><span class="nx">doorbellService</span><span class="p">);</span>


<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Registering info service…</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">infoService</span> <span class="o">=</span> <span class="nx">accessory</span><span class="p">.</span><span class="nf">getService</span><span class="p">(</span><span class="nx">Service</span><span class="p">.</span><span class="nx">AccessoryInformation</span><span class="p">)</span>
<span class="nx">infoService</span>
    <span class="p">.</span><span class="nf">setCharacteristic</span><span class="p">(</span><span class="nx">Characteristic</span><span class="p">.</span><span class="nx">Manufacturer</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Your Name</span><span class="dl">'</span><span class="p">)</span> <span class="c1">// add your name here</span>
    <span class="p">.</span><span class="nf">setCharacteristic</span><span class="p">(</span><span class="nx">Characteristic</span><span class="p">.</span><span class="nx">Model</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Doorbell</span><span class="dl">'</span><span class="p">)</span> <span class="c1">// The model can be whatever you want</span>
    <span class="p">.</span><span class="nf">setCharacteristic</span><span class="p">(</span><span class="nx">Characteristic</span><span class="p">.</span><span class="nx">SerialNumber</span><span class="p">,</span> <span class="dl">'</span><span class="s1">42</span><span class="dl">'</span><span class="p">)</span> <span class="c1">// You can also decide the serial number</span>
    <span class="p">.</span><span class="nf">setCharacteristic</span><span class="p">(</span><span class="nx">Characteristic</span><span class="p">.</span><span class="nx">FirmwareRevision</span><span class="p">,</span> <span class="dl">'</span><span class="s1">bookworm</span><span class="dl">'</span><span class="p">)</span> <span class="c1">// I used the os version of the Pi zero here, feel free to use whatever you want</span>
    <span class="p">.</span><span class="nf">setCharacteristic</span><span class="p">(</span><span class="nx">Characteristic</span><span class="p">.</span><span class="nx">HardwareRevision</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Mk1</span><span class="dl">'</span><span class="p">)</span> <span class="c1">// This can be also whatever you want</span>
    <span class="p">.</span><span class="nf">setCharacteristic</span><span class="p">(</span><span class="nx">Characteristic</span><span class="p">.</span><span class="nx">SoftwareRevision</span><span class="p">,</span> <span class="nx">pkg</span><span class="p">.</span><span class="nx">version</span><span class="p">)</span> <span class="c1">// I used the "version" property of the package.json here for clarity</span>


<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Registering indentify service…</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">accessory</span><span class="p">.</span><span class="nf">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">identify</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">paired</span><span class="p">,</span> <span class="nx">callback</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nf">warn</span><span class="p">(</span><span class="dl">'</span><span class="s1">Identify is not yet implemented in hardware!</span><span class="dl">'</span><span class="p">);</span>
    <span class="c1">// If you add an LED or something similar to help with identification you can toggle it in this handler.</span>
    <span class="nf">callback</span><span class="p">()</span>
<span class="p">})</span>


<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Initializing GPIO…</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">PigpioClient</span><span class="p">.</span><span class="nf">pigpio</span><span class="p">({</span> <span class="na">host</span><span class="p">:</span> <span class="dl">'</span><span class="s1">::1</span><span class="dl">'</span><span class="p">,</span> <span class="na">port</span><span class="p">:</span> <span class="mi">8888</span> <span class="p">});</span>
<span class="nx">client</span><span class="p">.</span><span class="nf">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">error</span><span class="dl">'</span><span class="p">,</span> <span class="nx">err</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">GPIO Error:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">err</span><span class="p">);</span>
    <span class="nx">process</span><span class="p">.</span><span class="nf">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">client</span><span class="p">.</span><span class="nf">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">connected</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Connected to pigpiod…</span><span class="dl">'</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">button</span> <span class="o">=</span> <span class="nx">client</span><span class="p">.</span><span class="nf">gpio</span><span class="p">(</span><span class="mi">23</span><span class="p">);</span> <span class="c1">// GPIO 23 has ID 535 internally and is directly next to a GND, </span>
                                    <span class="c1">// see the output of "cat /sys/kernel/debug/gpio" for more information</span>
                                    <span class="c1">// on the numbering scheme.</span>
    <span class="nx">button</span><span class="p">.</span><span class="nf">modeSet</span><span class="p">(</span><span class="dl">'</span><span class="s1">input</span><span class="dl">'</span><span class="p">);</span>
    <span class="nx">button</span><span class="p">.</span><span class="nf">pullUpDown</span><span class="p">(</span><span class="mi">2</span><span class="p">);</span>
    <span class="kd">let</span> <span class="nx">lastTick</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="nx">button</span><span class="p">.</span><span class="nf">notify</span><span class="p">((</span><span class="nx">level</span><span class="p">,</span> <span class="nx">tick</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="k">if </span><span class="p">(</span><span class="nx">level</span> <span class="o">===</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="nx">tick</span> <span class="o">-</span> <span class="nx">lastTick</span> <span class="o">&gt;</span> <span class="mi">200</span><span class="nx">_000</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Doorbell pressed!</span><span class="dl">'</span><span class="p">);</span>
            <span class="nx">triggerCharacteristic</span><span class="p">.</span><span class="nf">updateValue</span><span class="p">(</span><span class="nx">Characteristic</span><span class="p">.</span><span class="nx">ProgrammableSwitchEvent</span><span class="p">.</span><span class="nx">SINGLE_PRESS</span><span class="p">);</span>
            <span class="nx">lastTick</span> <span class="o">=</span> <span class="nx">tick</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">});</span>
    <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">GPIO Initialized!</span><span class="dl">'</span><span class="p">);</span>


    <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Publishing HomeKit service…</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">accessory</span><span class="p">.</span><span class="nf">publish</span><span class="p">({</span> <span class="c1">// Publish should always be the last step!</span>
        <span class="na">username</span><span class="p">:</span> <span class="dl">"</span><span class="s2">FE:ED:BE:EF:42:42</span><span class="dl">"</span><span class="p">,</span> <span class="c1">// this value should be customized and unique on your network in case you build multiple accessories (you could run them on the same host)</span>
        <span class="na">pincode</span><span class="p">:</span> <span class="dl">"</span><span class="s2">123-32-123</span><span class="dl">"</span><span class="p">,</span> <span class="c1">// you can change this to whatever value you want, this needs to be entered during setup in the Home app</span>
        <span class="na">port</span><span class="p">:</span> <span class="mi">47128</span><span class="p">,</span> <span class="c1">// remember to open this port if you use a firewall on the Pi Zero</span>
        <span class="na">category</span><span class="p">:</span> <span class="nx">Categories</span><span class="p">.</span><span class="nx">DOOR</span><span class="p">,</span> <span class="c1">// this is used for UI icons only, door fits best in my opinion</span>
    <span class="p">}).</span><span class="nf">then</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Accessory ready!</span><span class="dl">"</span><span class="p">);</span>
    <span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div> <p>In the code, GPIO 23 is used as sensing pin listening for the falling edge, it is debounced for 200ms before it triggers (you could also increase this to prevent spam) and then sends the HomeKit event.</p> <h3 id="doorbellservice">Doorbell.service</h3> <p>And finally, a SystemD service file so the HomeKit service starts when you boot the Pi:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Unit]
Description=Doorbell HomeKit Service
After=syslog.target
After=network.target
# Add the targets below if you use WiFi with dhcp…
#After=wpa_supplicant.service
#After=dhclient.service

[Service]
LimitMEMLOCK=infinity
LimitNOFILE=65535
RestartSec=2s
Type=simple
User=doorbell
Group=doorbell
WorkingDirectory=/home/doorbell/doorbell/
ExecStart=/usr/local/bin/node src/main.js
Restart=always
Environment=USER=doorbell

[Install]
WantedBy=multi-user.target
</code></pre></div></div> <p>As you can see in the SystemD file, I am running the service as the “doorbell” user. This user of course has to be created first (<code class="language-plaintext highlighter-rouge">adduser doorbell</code>), remember to follow best practices and set a strong password.</p> <h2 id="deployment">Deployment</h2> <p>Of course, you also need to transfer the code to the Pi zero somehow, for example you could either develop directly on there (code will be lost if the system breaks), or you develop using Git and then use the Pi as a remote you push to in order to deploy new code.</p> <p>I have a GitLab instance running in the network the Pi will run in, so I decided to do automated deployment using GitLab CI. First of all, I have to generate a new SSH Key pair, the public key of which I then add to the <code class="language-plaintext highlighter-rouge">authorized_keys</code> files of the <code class="language-plaintext highlighter-rouge">root</code> and <code class="language-plaintext highlighter-rouge">doorbell</code> user (root is needed to restart the service and configure the GPIO pin), and then I saved the private key in base64 encoded form as a masked and protected secret variable in GitLab CI. Finally, I added this CI file to the Git Repo (you will have to change <code class="language-plaintext highlighter-rouge">doorbell.local</code> to the DNS entry of the Pi):</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">image</span><span class="pi">:</span> <span class="s">debian:latest</span>

<span class="na">stages</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">deploy</span>

<span class="na">variables</span><span class="pi">:</span>
  <span class="na">JEKYLL_ENV</span><span class="pi">:</span> <span class="s">production</span>
  <span class="na">LC_ALL</span><span class="pi">:</span> <span class="s">C.UTF-8</span>

<span class="na">before_script</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">apt -yqqqqqqqqq update</span>
  <span class="pi">-</span> <span class="s">apt -yqqqqqqqqq upgrade</span>
  <span class="pi">-</span> <span class="s">apt -yqqqqqqqqq install rsync openssh-client</span>
  <span class="pi">-</span> <span class="s">mkdir -p ~/.ssh</span>
  <span class="pi">-</span> <span class="s">echo "$DEPLOY_KEY" | base64 -d &gt; ~/.ssh/id_ed25519</span>
  <span class="pi">-</span> <span class="s">chmod 600 ~/.ssh/id_ed25519</span>
  <span class="pi">-</span> <span class="s">ssh-keyscan -H doorbell.local &gt;&gt; ~/.ssh/known_hosts</span>

<span class="na">deploy-prod</span><span class="pi">:</span>
  <span class="na">stage</span><span class="pi">:</span> <span class="s">deploy</span>
  <span class="na">script</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">ssh -tn root@doorbell.local "systemctl enable --now pigpiod.service"</span>
    <span class="pi">-</span> <span class="s">ssh -tn root@doorbell.local "systemctl stop doorbell.service"</span>
    <span class="pi">-</span> <span class="s">ssh -tn doorbell@doorbell.local "mkdir -p /home/doorbell/doorbell/"</span>
    <span class="pi">-</span> <span class="s">rsync -avz --delete ./ doorbell@doorbell.local:/home/doorbell/doorbell/</span>
    <span class="pi">-</span> <span class="s">ssh -tn doorbell@doorbell.local "bash -lc 'cd ~/doorbell &amp;&amp; npm i'"</span>
    <span class="pi">-</span> <span class="s">ssh -tn root@doorbell.local "mv /home/doorbell/doorbell/doorbell.service /lib/systemd/system/doorbell.service"</span>
    <span class="pi">-</span> <span class="s">ssh -tn root@doorbell.local "systemctl daemon-reload"</span>
    <span class="pi">-</span> <span class="s">ssh -tn root@doorbell.local "systemctl restart doorbell.service"</span>
    <span class="pi">-</span> <span class="s">ssh -tn root@doorbell.local "systemctl enable doorbell.service"</span>
  <span class="na">rules</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">if</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$CI_COMMIT_TAG'</span>
      <span class="na">when</span><span class="pi">:</span> <span class="s">always</span>
    <span class="pi">-</span> <span class="na">when</span><span class="pi">:</span> <span class="s">never</span>
</code></pre></div></div> <p>This CI file copies files to the Pi, installs dependencies, and restarts the service. Plus it only runs on tagged commits.</p> <p>So now I am able to work on the code, push every commit, and when I want to update the code running on the doorbell, I just bump the version, add a tag, push it and everything will be done automatically. I even added a way to notified via Grafana Alerts when the service fails, if this sounds interesting for your case too, check out <a href="https://lerks.blog/p/how-to-monitor-dedicated-servers-imho#systemd_exporter">my post about monitoring servers</a>.</p> <h2 id="homekit-setup">HomeKit Setup</h2> <p>I already have an existing HomeKit setup using my AppleTV as a Home Hub, so I only needed to go to the “Add Accessory” screen, then (when the QR scanner view is shown) you have to select “more options”, and the Pi should be already visible there (the QR setup is only really needed when the accessory is not yet connected to the local network), and you can set it up using the <code class="language-plaintext highlighter-rouge">pincode</code> property set in the Node.js code. After a short waiting time, the accessory appears in the home view (alongside a notice that it’s not supported, since as a sad single developer I don’t have the resources required to join the MFi program) and is usable.</p> <h2 id="it-works">It works</h2> <p>When I now connect the GPIO I used in the code to a GND pin, (for example by using the doorbell button) I receive a notification on my iPhone: <img src="/assets/images/doorbell.jpg" alt="Screenshot of the &quot;doorbell rang&quot; notification on iOS" /></p> <hr /> <p>I hope this post was useful. If you have any suggestions or found errors please <a href="https://support.lrk.sh/blog">let me know</a>!</p> ]]> </content> <author> <name>Lerk</name> </author><category term="linux"/><category term="software"/><category term="how-to"/><category term="homekit"/><category term="raspberrypi"/><category term="javascript"/></entry><entry> <title>I still use X11 (and need it)</title> <id>https://lerks.blog/p/x</id> <updated>2025-07-01T07:00:00+00:00</updated> <link href="https://lerks.blog/p/x" rel="alternate" type="text/html" title="I still use X11 (and need it)"/><summary type="html"> <![CDATA[<p>Why is it currently trending that X11 is “dead”?I still use it, never even tried Wayland…</p>]]> </summary><content type="html" xml:base="https://lerks.blog/"> <![CDATA[<p>Why is it currently trending that X11 is “dead”? I still use it, never even tried Wayland…</p> <p>I don’t always use Linux GUI tools, but when I do, I most likely forward them to macOS using <a href="https://www.xquartz.org">XQuartz</a> and <code class="language-plaintext highlighter-rouge">ssh -YC</code> or <code class="language-plaintext highlighter-rouge">ssh -XC</code> (X11 forwarding and compression).</p> <p>Killing X11 will <a href="https://xkcd.com/1172/">break my workflow</a>, and I can only hope people will at least keep X11 (and XQuartz) alive in some state that allows continued usage in the places where wayland isn’t yet usable.</p> <p>If you have any suggestions (especially if you know how to do the X11&lt;-&gt;XQuartz equivalent with wayland) please <a href="https://support.lrk.sh/blog">let me know</a>!</p> <h2 id="update">Update</h2> <p>I found a way of doing the forwarding from within a wayland session using XWayland, so the problem I see seems solvable, or at least circumventable, although it’s still not clear to me if this also applies to an otherwise completely GUI-less system and if I need to do any extra configuration on my servers in order to get the forwarding work as conveniently as it currently is. And while there is an <a href="https://github.com/owl-compositor/owl">Objective-C wayland client</a> on GitHub, the last commit was four years ago, the README states it’s still in early development, and I only know Swift, so I’d have to learn Objective-C <abbr title="Thinking about this gave me uncomfortable memories about writing a calculator app for iPhone OS 3">again</abbr> before being able to try to work on anything in that direction.</p> <p>To be honest, I think I will leave this work for future Lerk in a time when X11 forwarding doesn’t work anymore and the changes have to be made anyway (and maybe the forwarding is properly included by then, which would be a perfect outcome and solve the reason for this rant, a (in my opinion) core feature missing in the replacement of something considered legacy).</p> ]]> </content> <author> <name>Lerk</name> </author><category term="linux"/><category term="software"/><category term="rant"/></entry><entry> <title>How To Host Your Own APT Mirror</title> <id>https://lerks.blog/p/self-hosted-apt-mirror</id> <updated>2025-06-08T14:00:00+00:00</updated> <link href="https://lerks.blog/p/self-hosted-apt-mirror" rel="alternate" type="text/html" title="How To Host Your Own APT Mirror"/><summary type="html"> <![CDATA[<p>In this post I will describe the necessary steps to host your own APT mirror that you can use in your local network to speed up package installations and updates, and save bandwidth.</p>]]> </summary><content type="html" xml:base="https://lerks.blog/"> <![CDATA[<p>In this post I will describe the necessary steps to host your own APT mirror that you can use in your local network to speed up package installations and updates, and save bandwidth.</p> <h2 id="requirements">Requirements</h2> <ul> <li>A Debian server/VM <ul> <li>See my <a href="/p/how-to-securely-set-up-a-new-server">post about remotely setting up an encrypted Debian server</a></li> <li>This guide <em>might</em> work with Ubuntu as well, although I don’t use it and didn’t test this guide with it.</li> </ul> </li> <li>About ~750GB of HDD space (more if you want to host multiple architectures and/or more repositories)</li> </ul> <h2 id="installation">Installation</h2> <p>Installing the system itself is outside the scope of this post, you can refer to the post linked above for a guide on installing Debian remotely, you can use a different method if you prefer, as long as you have a functioning system in the end.</p> <h3 id="setting-up-apt-mirror">Setting up <code class="language-plaintext highlighter-rouge">apt-mirror</code></h3> <p>After you installed Debian and did the initial hardening/configuration according to your needs, you can start with installing the dependencies.</p> <ol> <li>Install <code class="language-plaintext highlighter-rouge">apt-mirror</code>: <code class="language-plaintext highlighter-rouge">apt install apt-mirror apt-transport-https ca-certificates</code></li> <li>Configure <code class="language-plaintext highlighter-rouge">apt-mirror</code>: <ol> <li>Open the configuration file <code class="language-plaintext highlighter-rouge">/etc/apt/mirror.list</code> using your favorite editor and paste the following: <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>############# config ##################
        
 ## This option sets the base path for apt-mirror to use.
 set base_path    /var/apt-mirror
        
 ## The options below allow you to change the locations
 ## that are usually located in the base_path.
 # set mirror_path  $base_path/mirror
 # set skel_path    $base_path/skel
 # set var_path     $base_path/var
 # set cleanscript $var_path/clean.sh
        
 ## You can change the default architecture using this option.
 # set defaultarch  &lt;running host architecture&gt;
        
 ## You can use the options below to run a script
 ## after the mirroring finished.
 # set postmirror_script $var_path/postmirror.sh
 # set run_postmirror 0
        
 ## The number of threads to use for downloading packages. 
 set nthreads 8
        
 ## Packages with a tilde (~) in their version are pre-release,
 ## we don't want them in the mirror.
 set _tilde 0
        
 ############# end config ##############
        
 ##
 # Debian v12 Bookworm
 ##
        
 # Debian Bookworm
 deb https://deb.debian.org/debian bookworm main contrib non-free non-free-firmware
 deb-src https://deb.debian.org/debian bookworm main contrib non-free non-free-firmware
 deb-amd64 https://deb.debian.org/debian bookworm main contrib non-free non-free-firmware
 # deb-arm64 https://deb.debian.org/debian bookworm main contrib non-free non-free-firmware
        
 # Debian Bookworm Updates
 deb https://deb.debian.org/debian bookworm-updates main contrib non-free non-free-firmware
 deb-src https://deb.debian.org/debian bookworm-updates main contrib non-free non-free-firmware
 deb-amd64 https://deb.debian.org/debian bookworm-updates main contrib non-free non-free-firmware
 # deb-arm64 https://deb.debian.org/debian bookworm-updates main contrib non-free non-free-firmware
        
 # Debian Bookworm Backports
 deb https://deb.debian.org/debian bookworm-backports main contrib non-free non-free-firmware
 deb-src https://deb.debian.org/debian bookworm-backports main contrib non-free non-free-firmware
 deb-amd64 https://deb.debian.org/debian bookworm-backports main contrib non-free non-free-firmware
 # deb-arm64 https://deb.debian.org/debian bookworm-backports main contrib non-free non-free-firmware
        
 # Debian Bookworm Security
 deb https://deb.debian.org/debian-security bookworm-security main contrib non-free non-free-firmware
 deb-src https://deb.debian.org/debian-security bookworm-security main contrib non-free non-free-firmware
 deb-amd64 https://deb.debian.org/debian-security bookworm-security main contrib non-free non-free-firmware
 # deb-arm64 https://deb.debian.org/debian-security bookworm-security main contrib non-free non-free-firmware
        
 ##
 # Other Stuff
 ##
        
 # Clean Scripts
 clean https://deb.debian.org/debian-security
</code></pre></div> </div> </li> <li>Make sure to update the configuration according to your needs, especially the <code class="language-plaintext highlighter-rouge">base_path</code> and <code class="language-plaintext highlighter-rouge">nthreads</code> options, in case you want/need to use a different storage path or more threads to sync the mirror.</li> <li>In case you want to also mirror <code class="language-plaintext highlighter-rouge">arm64</code> packages, you can uncomment the <code class="language-plaintext highlighter-rouge">deb-arm64</code> statements in the repo list.</li> </ol> </li> <li>Add a CRON Job for the mirroring: <ol> <li>Use <code class="language-plaintext highlighter-rouge">crontab -e</code> to open the crontab editor using your favorite editor</li> <li>Add: <code class="language-plaintext highlighter-rouge">0 1 * * * /usr/bin/apt-mirror &gt; /var/log/apt-mirror.log</code></li> <li>This will run every day at <abbr title="Using 24-hour format…">1:00</abbr>, you can of course change this as you want.</li> </ol> </li> <li>Run the first mirroring (manually) <ol> <li>I’d recommend using screen for this (or any other similar tool you prefer): <code class="language-plaintext highlighter-rouge">screen -S first-mirror</code></li> <li>Run <code class="language-plaintext highlighter-rouge">apt-mirror</code> and wait until the execution finishes</li> </ol> </li> </ol> <h4 id="regarding-https">Regarding HTTPS</h4> <p>There are <a href="https://news.ycombinator.com/item?id=18958679">ongoing</a> <a href="https://www.reddit.com/r/linux/comments/aidxwa/comment/een1jsl/">discussions</a> <a href="https://askubuntu.com/questions/352952/are-repository-lists-secure-is-there-an-https-version">on</a> <a href="https://github.com/hestiacp/hestiacp/issues/695">various</a> <a href="https://security.stackexchange.com/a/248420">websites</a> about whether APT should use HTTPS/TLS by default (currently, it does not). I used HTTPS URLs for the mirror in this guide to provide a secure default, but the webserver of the mirror itself (which is assumed to run in the local network) is running with HTTP in order to keep the guide simple. You can find some examples of configuring NGINX for TLS on this blog (for example you could search for 443 to find config examples).</p> <h3 id="setting-up-nginx">Setting up NGINX</h3> <p>Now that the mirror content is synced, you need a way to fetch the mirrored content from your server. NGINX will be used in this guide together with the <code class="language-plaintext highlighter-rouge">fancyindex</code> module in order to render nice looking indexes.</p> <ol> <li>Install NGINX: <code class="language-plaintext highlighter-rouge">apt install nginx libnginx-mod-http-fancyindex</code></li> <li>Configure NGINX <ol> <li>Open the default config at <code class="language-plaintext highlighter-rouge">/etc/nginx/sites-enabled/default</code> using your favorite editor</li> <li>Paste the following: <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>server {
 listen 80 default_server;
 server_name _;

 access_log /var/log/nginx/mirror.access.log;
 error_log  /var/log/nginx/mirror.error.log;

 server_name_in_redirect off;

 autoindex off;
 server_tokens off;

 root /var/www/html;

 location /debian {
     alias /var/apt-mirror/mirror/deb.debian.org/debian;

     fancyindex on;
     fancyindex_exact_size off;
     fancyindex_header /head.html;
     fancyindex_footer /foot.html;
 }

 location /debian-security {
     alias /var/apt-mirror/mirror/deb.debian.org/debian-security;

     fancyindex on;
     fancyindex_exact_size off;
     fancyindex_header /head.html;
     fancyindex_footer /foot.html;
 }

 error_page 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 421 422 423 424 425 426 428 429 431 451 500 501 502 503 504 505 506 507 508 510 511 /error.html;

 location /error.html {
     root /var/www/html;
     internal;
 }
}
</code></pre></div> </div> </li> <li>Don’t forget to update the paths of the <code class="language-plaintext highlighter-rouge">alias</code> params in case you changed the path in the <code class="language-plaintext highlighter-rouge">apt-mirror</code> config</li> </ol> </li> <li>Add the HTML files (I’ll provide unstyled examples): <ol> <li>Add <code class="language-plaintext highlighter-rouge">/var/www/html/index.html</code> (replace <code class="language-plaintext highlighter-rouge">MIRROR_DOMAIN</code> with the actual DNS name (or IP(?)) of the mirror: <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="cp">&lt;!DOCTYPE html&gt;</span>
  <span class="nt">&lt;html</span> <span class="na">lang=</span><span class="s">"en"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;head&gt;</span>
      <span class="nt">&lt;title&gt;</span>APT Mirror<span class="nt">&lt;/title&gt;</span>
  <span class="nt">&lt;/head&gt;</span>
  <span class="nt">&lt;body&gt;</span>
  <span class="nt">&lt;h1&gt;</span>APT Mirror<span class="nt">&lt;/h1&gt;</span>
  <span class="nt">&lt;p&gt;</span>This is an APT mirror.<span class="nt">&lt;/p&gt;</span>
  <span class="nt">&lt;h2&gt;</span>Currently hosting:<span class="nt">&lt;/h2&gt;</span>
  <span class="nt">&lt;ul&gt;</span>
      <span class="nt">&lt;li&gt;</span>
          <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"/debian"</span><span class="nt">&gt;</span>/debian<span class="nt">&lt;/a&gt;</span>
          <span class="nt">&lt;ul&gt;</span>
              <span class="nt">&lt;li&gt;</span>bookworm<span class="nt">&lt;/li&gt;</span>
              <span class="nt">&lt;li&gt;</span>bookworm-updates<span class="nt">&lt;/li&gt;</span>
              <span class="nt">&lt;li&gt;</span>bookworm-backports<span class="nt">&lt;/li&gt;</span>
          <span class="nt">&lt;/ul&gt;</span>
      <span class="nt">&lt;/li&gt;</span>
      <span class="nt">&lt;li&gt;</span>
          <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"/debian-security"</span><span class="nt">&gt;</span>/debian-security<span class="nt">&lt;/a&gt;</span>
          <span class="nt">&lt;ul&gt;</span>
              <span class="nt">&lt;li&gt;</span>bookworm-security<span class="nt">&lt;/li&gt;</span>
          <span class="nt">&lt;/ul&gt;</span>
      <span class="nt">&lt;/li&gt;</span>
  <span class="nt">&lt;/ul&gt;</span>
  <span class="nt">&lt;h2&gt;</span>Usage<span class="nt">&lt;/h2&gt;</span>
  <span class="nt">&lt;p&gt;</span>Select the repos you need and add them to your <span class="nt">&lt;code&gt;</span>/etc/apt/sources.list<span class="nt">&lt;/code&gt;</span>:<span class="nt">&lt;/p&gt;</span>
  <span class="nt">&lt;details&gt;</span>
      <span class="nt">&lt;summary&gt;</span>Debian v12 (bookworm)<span class="nt">&lt;/summary&gt;</span>
      <span class="nt">&lt;pre&gt;</span>
  # Debian Bookworm
  deb http://MIRROR_DOMAIN/debian bookworm main contrib non-free non-free-firmware
  deb-src http://MIRROR_DOMAIN/debian bookworm main contrib non-free non-free-firmware
        
  # Debian Bookworm Updates
  deb http://MIRROR_DOMAIN/debian bookworm-updates main contrib non-free non-free-firmware
  deb-src http://MIRROR_DOMAIN/debian bookworm-updates main contrib non-free non-free-firmware
        
  # Debian Bookworm Security
  deb http://MIRROR_DOMAIN/debian-security bookworm-security main contrib non-free non-free-firmware
  deb-src http://MIRROR_DOMAIN/debian-security bookworm-security main contrib non-free non-free-firmware
        
  # Debian Bookworm Backports
  deb http://MIRROR_DOMAIN/debian bookworm-backports main
  deb-src http://MIRROR_DOMAIN/debian bookworm-backports main
          <span class="nt">&lt;/pre&gt;</span>
  <span class="nt">&lt;/details&gt;</span>
  <span class="nt">&lt;/body&gt;</span>
  <span class="nt">&lt;/html&gt;</span>
</code></pre></div> </div> </li> <li>Add <code class="language-plaintext highlighter-rouge">/var/www/html/error.html</code>: <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="cp">&lt;!DOCTYPE html&gt;</span>
  <span class="nt">&lt;html</span> <span class="na">lang=</span><span class="s">"en"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;head&gt;</span>
      <span class="nt">&lt;title&gt;</span>APT Mirror<span class="nt">&lt;/title&gt;</span>
  <span class="nt">&lt;/head&gt;</span>
  <span class="nt">&lt;body&gt;</span>
  <span class="nt">&lt;h1&gt;</span>Error!<span class="nt">&lt;/h1&gt;</span>
  <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"lead"</span><span class="nt">&gt;</span>An error occurred.<span class="nt">&lt;/p&gt;</span>
  <span class="nt">&lt;p&gt;&lt;a</span> <span class="na">href=</span><span class="s">"/"</span><span class="nt">&gt;</span>Main Page<span class="nt">&lt;/a&gt;</span>
  <span class="nt">&lt;/body&gt;</span>
  <span class="nt">&lt;/html&gt;</span>
</code></pre></div> </div> </li> <li>Add <code class="language-plaintext highlighter-rouge">/var/www/html/head.html</code>: <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nt">&lt;html</span> <span class="na">lang=</span><span class="s">"en"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;head&gt;</span>
      <span class="nt">&lt;title&gt;</span>APT Mirror<span class="nt">&lt;/title&gt;</span>
  <span class="nt">&lt;/head&gt;</span>
  <span class="nt">&lt;body&gt;</span>
  <span class="nt">&lt;h1&gt;</span>APT Mirror<span class="nt">&lt;/h1&gt;</span>
  <span class="nt">&lt;p&gt;</span>Welcome to the APT Mirror.<span class="nt">&lt;/p&gt;</span>
  <span class="nt">&lt;h1&gt;</span>
</code></pre></div> </div> <ul> <li>This gets cut off at the open <code class="language-plaintext highlighter-rouge">&lt;h1&gt;</code> because the <code class="language-plaintext highlighter-rouge">fancyindex</code> module will render the title of the folder it’s showing after that and close it.</li> </ul> </li> <li>Add <code class="language-plaintext highlighter-rouge">/var/www/html/foot.html</code>: <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nt">&lt;hr</span> <span class="nt">/&gt;</span>
  <span class="nt">&lt;p&gt;</span>Thanks for using the APT Mirror.<span class="nt">&lt;/p&gt;</span>
  <span class="nt">&lt;/body&gt;</span>
  <span class="nt">&lt;/html&gt;</span>
</code></pre></div> </div> </li> <li>Remove <code class="language-plaintext highlighter-rouge">/var/www/html/index.nginx-debian.html</code> in case it exists</li> <li>Ensure the files are not world-writable and belong to either root or www-data</li> </ol> </li> <li>Confirm that the NGINX config is valid by running: <code class="language-plaintext highlighter-rouge">nginx -t</code></li> <li>Restart NGINX: <code class="language-plaintext highlighter-rouge">systemctl restart nginx</code></li> </ol> <p>You should now be able to open <code class="language-plaintext highlighter-rouge">http://MIRROR_DOMAIN/</code> (changed to your actual DNS name) in a browser and also view the repo contents. I added example usage instructions into the example <code class="language-plaintext highlighter-rouge">index.html</code>, so you can copy the example <code class="language-plaintext highlighter-rouge">sources.list</code> contents from there.</p> <p>The next step would be adding styles to the mirror pages so it looks better, and of course add more/different repos/architectures in case you need them.</p> <hr /> <p>I hope this post was useful. If you have any suggestions or found errors please <a href="https://support.lrk.sh/blog">let me know</a>!</p> ]]> </content> <author> <name>Lerk</name> </author><category term="how-to"/><category term="debian"/><category term="linux"/><category term="apt"/><category term="mirror"/><category term="software"/><category term="server"/></entry><entry> <title>Reboot your TV Remote</title> <id>https://lerks.blog/p/reboot-your-tv-remote</id> <updated>2025-05-24T13:20:00+00:00</updated> <link href="https://lerks.blog/p/reboot-your-tv-remote" rel="alternate" type="text/html" title="Reboot your TV Remote"/><summary type="html"> <![CDATA[<p>If you suddenly can’t control your TV volume using your Apple TV Remote anymore, you might need to reboot it by holding “TV/Screen” and “Volume Down” for ~10 seconds…</p>]]> </summary><content type="html" xml:base="https://lerks.blog/"> <![CDATA[<p>If you suddenly can’t control your TV volume using your Apple TV Remote anymore, you might need to reboot it by holding “TV/Screen” and “Volume Down” for ~10 seconds…</p> <p style="text-align: center;"> <abbr style="text-decoration: none; cursor: not-allowed; user-select: none;" title="I want a root shell on this thing now, it probably could do usb ethernet or at least wifi so I could host this blog on a cluster of Apple TV remotes!"> 🙃 </abbr> </p> ]]> </content> <author> <name>Lerk</name> </author><category term="apple"/><category term="tv"/><category term="remote"/><category term="tech"/><category term="nightmare"/></entry><entry> <title>Audio/Music Player for iOS</title> <id>https://lerks.blog/p/muzik</id> <updated>2025-05-17T13:03:00+00:00</updated> <link href="https://lerks.blog/p/muzik" rel="alternate" type="text/html" title="Audio/Music Player for iOS"/><summary type="html"> <![CDATA[<p>I made an audio player app for iOS which plays the locally synced library.</p>]]> </summary><content type="html" xml:base="https://lerks.blog/"> <![CDATA[<p>I made an audio player app for iOS which plays the locally synced library.</p> <p>The main motivation for this was the fact that in the default app, three of the four tabs are useless unless you use cloud music, which makes the (previously “preferred”) local library feel like a dropped potato.</p> <h2 id="features">Features</h2> <p>Please note that I glance over the main feature which is playing music.</p> <h3 id="no-ads-or-tracking">No Ads or Tracking</h3> <p>All with all of my apps, there are no ads, and it doesn’t contain any tracking functionality. The only things collected are the crash logs and installation count that apple automatically collects (there is no way to turn that off as far as I know; If there is a way, as always, let me know please). No screen space is wasted on annoying things and the app does not care whether you’re connected to the internet.</p> <h3 id="customizability">Customizability</h3> <p><img src="/assets/images/muzik-colors.png" alt="arrangement of screenshot slices showing different theme colors" /></p> <p>You can choose a theme color in the app settings, so the app appears how you like it.</p> <h3 id="search">Search</h3> <p><img src="/assets/images/muzik-search.png" alt="screenshot showing a search" /></p> <p>Views like the artists, album, and song lists are searchable.</p> <h3 id="library">Library</h3> <p>The “Library” tab currently shows the following:</p> <ul> <li>The top ten most listened to songs <ul> <li>The underlying queue is your Top 100, so if you play the top most played song, you will have 100 of your top songs in your queue.</li> </ul> </li> <li>Three random songs you’ve never listened to (if songs like that exist) (added in v1.1.0) <ul> <li>Same as with the Top 100 you will have up to 100 songs in the queue, but they are randomized each time the library refreshes.</li> </ul> </li> <li>The ten most recently added albums</li> </ul> <h3 id="added-in-v110-queue-management">(Added in v1.1.0) Queue Management</h3> <p><img src="/assets/images/muzik-queue.gif" alt="screen recording of queue management" /></p> <p>You can modify the queue and manage what is played later on.</p> <h3 id="added-in-v120-customizable-library">(Added in v1.2.0) Customizable Library</h3> <p>The library tab now has the following sections:</p> <ul> <li><em>Best Rated</em> <ul> <li>This tab shows the ten highest rated songs in your library and playing a song adds up to 100 songs with a rating over 0 (no rating) sorted by rating to the queue.</li> </ul> </li> <li><em>Last Played</em> <ul> <li>This tab shows the last played songs in your library sorted by last played date, it also adds 100 of the last played songs to the queue if you play one of the songs.</li> </ul> </li> <li><em>Most Played</em> <ul> <li>This tab shows the ten most played items in your library sorted by the play count. It also adds the top 100 most played songs to the queue.</li> </ul> </li> <li><em>Most Skipped</em> <ul> <li>This tab shows the ten most skipped songs in your library sorted by skip count. It also adds the top 100 most skipped songs to your queue.</li> </ul> </li> <li><em>Never Listened To</em> <ul> <li>This tab shows three random songs you have never listened to. It adds up to 100 songs without any plays in the same random order as shown in the library tab to the queue.</li> </ul> </li> <li><em>Recently Added</em> <ul> <li>This tab shows the ten most recently added albums.</li> </ul> </li> </ul> <p>You can set the visibility and order for each section in the newly added library customization settings.</p> <h3 id="added-in-v120-configurable-song-list-sort">(Added in v1.2.0) Configurable Song List Sort</h3> <p>Since v1.2.0 the default and current sort order of the song list can be changed in the settings and using the sort order control in the song list navbar.</p> <p>Depending on the chosen sort order, additional information is shown in the list, for example sorting by rating shows the rating, etc.</p> <h3 id="added-in-v130-new-search-experience">(Added in v1.3.0) New Search Experience</h3> <p>The search unifies the previously separate list views into a single tab, and the other lists are now sortable as well.</p> <p><img src="/assets/images/muzik-search-ios26.png" alt="screenshot of the new search, in song search mode, on iOS 26" /></p> <h3 id="added-in-v131-mini-player-gesture-support">(Added in v1.3.1) Mini Player Gesture Support</h3> <p>The mini player now supports swipe left/right gestures for next/previous track and swipe up (and/or tap) to open the main player view.</p> <h2 id="planned-updates">Planned Updates</h2> <p>If you want to help with testing the new features listed below, you can join the <a href="https://lrk.lol/muzik-beta">TestFlight Beta</a>.</p> <p><em>(Currently, there are no features planned, if you need something please <a href="https://support.lrk.sh/muzik">send in a request</a>)</em></p> <h2 id="download-link">Download Link</h2> <p>The app is completely free to use, and can be downloaded on the AppStore.</p> <p>I hope this app is useful, if not don’t hesitate to <a href="https://support.lrk.sh/muzik">tell me the ways I can improve it</a>.</p> <p><a href="https://apps.apple.com/us/app/audio-music-player/id6745765114?itscg=30200&amp;itsct=apps_box_badge&amp;mttnsubad=6745765114"> <img src="https://toolbox.marketingtools.apple.com/api/v2/badges/download-on-the-app-store/black/en-us?releaseDate=1747180800" alt="Download on the App Store" style="width: 245px; height: 82px; vertical-align: middle; object-fit: contain;" /> </a></p> ]]> </content> <author> <name>Lerk</name> </author><category term="ios"/><category term="app"/><category term="music"/><category term="tools"/></entry><entry> <title>Network Tools/Toolkit for iOS</title> <id>https://lerks.blog/p/nettools</id> <updated>2025-03-10T05:00:00+00:00</updated> <link href="https://lerks.blog/p/nettools" rel="alternate" type="text/html" title="Network Tools/Toolkit for iOS"/><summary type="html"> <![CDATA[<p>I wrote a network toolkit app as a weekend project, mainly because I needed one of those and when I searched for it, the first few results had ads, tracking and/or annoying cookie banners.</p>]]> </summary><content type="html" xml:base="https://lerks.blog/"> <![CDATA[<p>I wrote a network toolkit app as a weekend project, mainly because I needed one of those and when I searched for it, the first few results had ads, tracking and/or annoying cookie banners.</p> <p>I mean imagine your network is down, you just want to quickly check stuff while restoring it, and then half the screen is taken up by some ad that might not even load in case you’re offline so you’re just looking at a blank screen with a tiny speck of information, if you’re lucky enough that it’s not fully blocked, or you get a “subscription required” screen. Super annoying, to the point where you almost loose interest in fixing your issue, since you must code an app for that first.</p> <h2 id="features">Features</h2> <h3 id="no-ads-or-tracking">No Ads or Tracking</h3> <p>The app shows no ads and doesn’t contain any tracking functionality. The only thing collected are the crash logs and installation count that apple automatically collects (there is no way to turn that off as far as I know; If there is a way let me know please). No screen space is wasted on annoying things and the app does not care whether you’re connected to the internet.</p> <h3 id="network-information">Network information</h3> <p><img src="/assets/images/nettools-info.png" alt="screenshot of the network information section of the app" /></p> <p>The app can show network information like the state of the current connection, and the properties of the different interfaces configured on the device.</p> <p>Since v1.2 the app also can resolve the public IP of your device, although this feature is opt-in since a request has to be sent to a third party to do that.</p> <h3 id="ping">Ping</h3> <p><img src="/assets/images/nettools-ping.png" alt="screenshot of the ping section of the app" /></p> <p>You can ping target hosts and view result metrics like round trip time, and responding hostname/address.</p> <h3 id="traceroute">Traceroute</h3> <p><img src="/assets/images/nettools-traceroute.png" alt="screenshot of the traceroute section of the app" /></p> <p>You can run an ICMP-based traceroute to visualize the path(s) packets take to a target, with metrics like round trip time, and name resolution of responding hosts.</p> <h3 id="dns">DNS</h3> <p><img src="/assets/images/nettools-dns.png" alt="screenshot of the dns lookup section of the app" /></p> <p>You can run DNS lookups for various (or any/all) record types for any domain using either a default (currently 1.1.1.1) or custom name server.</p> <h3 id="port-scan-added-in-12">Port Scan (Added in 1.2)</h3> <p><img src="/assets/images/nettools-portscan.png" alt="screenshot of the port scan section of the app" /></p> <p>You can run port scans for any domain/ip using TCP, UDP or both with a custom port range, or all ports.</p> <h3 id="network-calculators-added-in-13">Network Calculators (Added in 1.3)</h3> <p><img src="/assets/images/nettools-calculators.png" alt="screenshot of the network calculator section of the app" /></p> <p>You can calculate various things like CIDR from a subnet mask, subnet ranges, and wildcard mask. Currently, the calculator can do IPv4 only.</p> <h3 id="result-export-added-in-14">Result Export (Added in 1.4)</h3> <p>The app now has an additional toolbar button that allows to export results as either JSON or “CSV” (separated with semicolon instead of comma) for the port scan and dns lookup.</p> <h2 id="planned-features">Planned Features</h2> <p>If you want to help with testing the planned updates listed below, feel free to join the <a href="https://lrk.lol/nettools-beta">TestFlight Beta</a>.</p> <p>I currently plan to add or improve the following things:</p> <ul class="task-list"> <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Improve DNS results <ul> <li>With some DNS responses, the values are formatted weirdly after parsing</li> <li>Some SOA results seem to be duplicated or should be organized differently</li> </ul> </li> <li class="task-list-item">Allow UDP/TCP “Pings”</li> <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Add History/Result Recorder</li> <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><del>Add Result Export</del> (Added in 1.4)</li> <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><del>Add port scanning functionality</del> (Added in 1.2)</li> <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><del>Add network calculators</del> (Added in 1.3)</li> </ul> <h2 id="download-link">Download Link</h2> <p>The app is completely free to use, and can be downloaded on the AppStore.</p> <p>I hope this app is useful, if not don’t hesitate to <a href="https://support.lrk.sh/nettools">tell me the ways I can improve it</a>.</p> <p><a href="https://apps.apple.com/us/app/network-tools-toolkit/id6743007628?itscg=30200&amp;itsct=apps_box_badge&amp;mttnsubad=6743007628"> <img src="https://toolbox.marketingtools.apple.com/api/v2/badges/download-on-the-app-store/black/en-us?releaseDate=1741478400" alt="Download on the App Store" style="width: 245px; height: 82px; vertical-align: middle; object-fit: contain;" /> </a></p> ]]> </content> <author> <name>Lerk</name> </author><category term="ios"/><category term="app"/><category term="network"/><category term="tools"/><category term="ping"/><category term="traceroute"/><category term="dns"/></entry><entry> <title>Calculating with different units in CSS</title> <id>https://lerks.blog/p/calculating-with-different-css-units</id> <updated>2024-09-21T19:51:00+00:00</updated> <link href="https://lerks.blog/p/calculating-with-different-css-units" rel="alternate" type="text/html" title="Calculating with different units in CSS"/><summary type="html"> <![CDATA[<p>Have you ever come upon the pleasure of doing math in your CSS or LESS code?</p>]]> </summary><content type="html" xml:base="https://lerks.blog/"> <![CDATA[<p>Have you ever come upon the pleasure of doing math in your CSS or LESS code?</p> <p>You might have noticed that this won’t work the way you probably tried it the first time. I tried something like this:</p> <div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">.my-element</span> <span class="p">{</span>
    <span class="nl">width</span><span class="p">:</span> <span class="m">100px</span> <span class="o">-</span> <span class="m">50vw</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div> <p>For me this was completely logical. The width of my element is half the viewport width (50vw) subtracted by 100 pixels. But any browser ignores the second unit (vh in this case) and just sticks with pixels. So the width of my element was 50 always pixels.</p> <p>TL;DR: There is a calc() function in CSS which does that for you. Just do it like this:</p> <div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">.my-element</span> <span class="p">{</span>
    <span class="nl">width</span><span class="p">:</span> <span class="nf">calc</span><span class="p">(</span><span class="m">100px</span> <span class="o">-</span> <span class="m">50vw</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div> ]]> </content> <author> <name>Lerk</name> </author><category term="web-design"/><category term="css"/><category term="how-to"/><category term="software"/><category term="snippets"/></entry><entry> <title>How to create a Windows USB Stick on macOS/Linux</title> <id>https://lerks.blog/p/windows-install-stick-macos</id> <updated>2023-03-28T20:02:00+00:00</updated> <link href="https://lerks.blog/p/windows-install-stick-macos" rel="alternate" type="text/html" title="How to create a Windows USB Stick on macOS/Linux"/><summary type="html"> <![CDATA[<p>In this post, I’ll describe how you can create a bootable Windows installation USB drive using macOS (and Linux too).</p>]]> </summary><content type="html" xml:base="https://lerks.blog/"> <![CDATA[<p>In this post, I’ll describe how you can create a bootable Windows installation USB drive using macOS (and Linux too).</p> <p>Please note that this guide is written primarily for macOS, you might need to adapt some parts on Linux. Disk Utility, for example, is not available on Linux, but you can replace it with gparted or a command line tool to achive the same effect.</p> <h2 id="prerequisites">Prerequisites</h2> <p>You’ll have to have the following:</p> <ul> <li>A <code class="language-plaintext highlighter-rouge">.iso</code> file of the version of Windows you want to install</li> <li>A USB drive, 8GB should be enough</li> <li>wimlib <ul> <li><a href="https://packages.debian.org/source/bullseye/wimlib">Debian package</a></li> <li><a href="https://formulae.brew.sh/formula/wimlib">Homebrew package</a></li> </ul> </li> <li>rsync <ul> <li><a href="https://packages.debian.org/source/bullseye/rsync">Debian package</a></li> <li><a href="https://formulae.brew.sh/formula/rsync">Homebrew package</a></li> </ul> </li> </ul> <h2 id="creating-the-usb-drive">Creating the USB drive</h2> <p>You can create the USB drive by using the following steps:</p> <ol> <li>Open Disk Utility and Erase the USB drive <ul> <li>You might need to use “View &gt; Show All Devices” in order to see the target drive itself instead of the volume</li> <li>Use FAT as partition format, and MBR instead of GUID</li> </ul> </li> <li>Mount the iso using the following command: <ul> <li><code class="language-plaintext highlighter-rouge">hdiutil mount /path/to/windows.iso</code></li> <li>Replace <code class="language-plaintext highlighter-rouge">/path/to/windows.iso</code> with the actual path of the <code class="language-plaintext highlighter-rouge">.iso</code> file</li> </ul> </li> <li>Copy the iso contents to the drive: <ul> <li><code class="language-plaintext highlighter-rouge">rsync -vha --exclude=sources/install.wim /Volumes/NAME_OF_ISO/* /Volumes/TARGET_DRIVE</code></li> <li>Replace <code class="language-plaintext highlighter-rouge">NAME_OF_ISO</code> with the name of the mounted volume (visible in Finder or Disk Utility)</li> <li>Replace <code class="language-plaintext highlighter-rouge">TARGET_DRIVE</code> with the name of the target drive volume (also visible in Finder or Disk Utility)</li> </ul> </li> <li>Rebuild/split the <code class="language-plaintext highlighter-rouge">install.wim</code> archive: <ul> <li><code class="language-plaintext highlighter-rouge">wimlib-imagex split /Volumes/NAME_OF_ISO/sources/install.wim /Volumes/TARGET_DRIVE/sources/install.swm 3800</code></li> <li>Replace <code class="language-plaintext highlighter-rouge">NAME_OF_ISO</code> with the name of the mounted volume (visible in Finder or Disk Utility)</li> <li>Replace <code class="language-plaintext highlighter-rouge">TARGET_DRIVE</code> with the name of the target drive volume (also visible in Finder or Disk Utility)</li> </ul> </li> <li>Unmount the target drive and use it to boot the Windows installer</li> </ol> <hr /> <p>If you have any improvements or comments, please <a href="https://support.lrk.sh/blog">let me know</a>!</p> ]]> </content> <author> <name>Lerk</name> </author><category term="how-to"/><category term="software"/><category term="windows"/><category term="boot-usb"/><category term="macos"/><category term="linux"/><category term="installer"/></entry><entry> <title>How to sync your Windows account picture using Active Directory</title> <id>https://lerks.blog/p/windows-ad-sync-account-pictures</id> <updated>2023-03-27T17:08:00+00:00</updated> <link href="https://lerks.blog/p/windows-ad-sync-account-pictures" rel="alternate" type="text/html" title="How to sync your Windows account picture using Active Directory"/><summary type="html"> <![CDATA[<p>In this post, I will show how you can sync your Windows account pictures using Active Directory and group policies.</p>]]> </summary><content type="html" xml:base="https://lerks.blog/"> <![CDATA[<p>In this post, I will show how you can sync your Windows account pictures using Active Directory and group policies.</p> <h2 id="prerequisites">Prerequisites</h2> <p>Let’s get a few things out of the way first:</p> <ul> <li>I don’t know if and how this applies to an Azure AD setup, I’m talking about a locally hosted Active Directory server</li> <li>The account pictures need to be in JPEG format, <strong>96x96px</strong> and not larger than <strong>100kb</strong> in size</li> <li>This apparently doesn’t work on Windows 11 (yet)</li> </ul> <p>Please keep the above points in mind.</p> <h2 id="create-the-group-policy">Create the group policy</h2> <p>The following steps are necessary to create the required group policy and add the necessary permissions:</p> <ol> <li>Connect to your domain controller and open the group policy manager, in there create a new group policy (use a descriptive name)</li> <li>Open/edit the policy and create the required registry key: <ol> <li>Navigate to <em>Computer Configuration/Policies/Windows Settings/Security Settings/Registry</em> using the tree view</li> <li>Right click into the registry list and create a new key with the path <code class="language-plaintext highlighter-rouge">MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AccountPicture\Users</code></li> <li>In the next window grant <em>Full Access</em> to the <code class="language-plaintext highlighter-rouge">&lt;DOMAIN&gt;\Users</code> group so that each user can set their account picture</li> <li>In the “Add Object” window, select <em>Replace existing permission on all subkeys with inheritable permissions</em></li> </ol> </li> <li>Next, navigate to <em>Computer Configuration/Administrative Templates/System/Group Policy</em> and set <em>Configure user Group Policy Loopback Processing mode</em> to <strong>Merge</strong></li> <li>You can now close the group policy for now (or leave it open, it’s a multi window os y’know)</li> </ol> <h3 id="create-the-updater-script">Create the updater script</h3> <p>The following PowerShell script should be saved on some accessible path on your domain controller, in my case its located at <code class="language-plaintext highlighter-rouge">\\&lt;DOMAIN&gt;\sysvol\&lt;DOMAIN&gt;\scripts\SetAccountPictureFromAD.ps1</code>:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> Function ResizeImage {
    Param (
        [Parameter(Mandatory = $True, HelpMessage = "The image as bytes")]
        [ValidateNotNull()]
        $imageSource,
        
        [Parameter(Mandatory = $true, HelpMessage = "The canvas size can be between 16px and 1000px")]
        [ValidateRange(16, 1000)]
        $canvasSize,
        
        [Parameter(Mandatory = $true, HelpMessage = "The image quality can be between 1 and 100")]
        [ValidateRange(1, 100)]
        $ImgQuality = 100
    )
    
    [void][System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
    $imageBytes = [byte[]]$imageSource
    $ms = New-Object IO.MemoryStream($imageBytes, 0, $imageBytes.Length)
    $ms.Write($imageBytes, 0, $imageBytes.Length);
    $bmp = [System.Drawing.Image]::FromStream($ms, $true)
    
    # Image size after conversion
    $canvasWidth = $canvasSize
    $canvasHeight = $canvasSize
    
    # Set picture quality
    $myEncoder = [System.Drawing.Imaging.Encoder]::Quality
    $encoderParams = New-Object System.Drawing.Imaging.EncoderParameters(1)
    $encoderParams.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter($myEncoder, $ImgQuality)
    
    # Get image type
    $myImageCodecInfo = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where-Object { $_.MimeType -eq 'image/jpeg' }
    
    # Get aspect ratio
    $ratioX = $canvasWidth / $bmp.Width;
    $ratioY = $canvasHeight / $bmp.Height;
    $ratio = $ratioY
    if ($ratioX -le $ratioY) {
        $ratio = $ratioX
    }
    
    # Create an empty picture
    $newWidth = [int] ($bmp.Width * $ratio)
    $newHeight = [int] ($bmp.Height * $ratio)
    $bmpResized = New-Object System.Drawing.Bitmap($newWidth, $newHeight)
    $graph = [System.Drawing.Graphics]::FromImage($bmpResized)
    $graph.Clear([System.Drawing.Color]::White)
    $graph.DrawImage($bmp, 0, 0 , $newWidth, $newHeight)
    
    # Create an empty stream
    $ms = New-Object IO.MemoryStream
    $bmpResized.Save($ms, $myImageCodecInfo, $($encoderParams))
    
    # cleanup
    $bmpResized.Dispose()
    $bmp.Dispose()
    return $ms.ToArray()
}

$ADUserInfo = ([ADSISearcher]"(&amp;(objectCategory=User)(SAMAccountName=$env:username))").FindOne().Properties
$ADUserInfo_sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value

If ($ADUserInfo.thumbnailphoto) {
    $img_sizes = @(32, 40, 48, 96, 192, 200, 240, 448)
    $img_base = "C:\Users\Public\AccountPictures"
    $reg_key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AccountPicture\Users\$ADUserInfo_sid"

    If ((Test-Path -Path $reg_key) -eq $false) { New-Item -Path $reg_key } { write-verbose "Registry key exists: [$reg_key]" }

    Try {
        ForEach ($size in $img_sizes) {
            $dir = $img_base + "\" + $ADUserInfo_sid
            If ((Test-Path -Path $dir) -eq $false) { $(New-Item -ItemType directory -Path $dir).Attributes = "Hidden" }
            $file_name = "Image$($size).jpg"
            $path = $dir + "\" + $file_name
            Write-Verbose " Create file: [$file_name]"
            
            try {
                ResizeImage -imageSource $($ADUserInfo.thumbnailphoto) -canvasSize $size -ImgQuality 100 | Set-Content -Path $path -Encoding Byte -Force -ErrorAction Stop
                Write-Verbose " File saved: [$file_name]"
            }
            catch {
                If (Test-Path -Path $path) {
                    Write-Warning "File exists: [$path]"
                }
                else {
                    Write-Warning "File doesn't exist: [$path]"
                }
            }
            
            $name = "Image$size"
            try {
                $null = New-ItemProperty -Path $reg_key -Name $name -Value $path -Force -ErrorAction Stop
            }
            catch {
                Write-Warning "Registry key edit error [$reg_key] [$name]"
            }
        }
    }
    Catch {
        Write-Error "Check the permissions to files or registry!"
    }
} 
</code></pre></div></div> <p>Make sure that the permissions of the script allow authenticated users to execute it!</p> <h3 id="adding-the-script-to-the-group-policy">Adding the script to the group policy</h3> <p>The following steps are necessary to add the script to the group policy:</p> <ol> <li>Open up the group policy again, or return to the editor window</li> <li>Navigate to <em>User Configuration/Policies/Windows Settings/Scripts (Logon/Logoff)</em> and double click <em>Logoff</em></li> <li>In the new window, <strong>change to the PowerShell Scripts tab</strong> and add the path to the script <ul> <li>In my case it’s located at <code class="language-plaintext highlighter-rouge">\\&lt;DOMAIN&gt;\sysvol\&lt;DOMAIN&gt;\scripts\SetAccountPictureFromAD.ps1</code></li> <li><strong>If you add the script to the wrong tab, the clients will “freeze”/take ages when logging out!</strong></li> </ul> </li> <li>Close the policy editor</li> </ol> <h3 id="activating-the-group-policy">Activating the group policy</h3> <p>To activate the group policy and start syncing pictures, you should drag/link the policy to an organisational unit that contains your client devices (not users).</p> <h2 id="adding-pictures-to-active-directory">Adding pictures to Active Directory</h2> <p>There are multiple ways to add the profile pictures to the Active Directory user accounts:</p> <ul> <li>You could open the properties of the account and add an image to the <code class="language-plaintext highlighter-rouge">thumbnailphoto</code> property using the UI (never tried that)</li> <li>You could use PowerShell: <ol> <li>Put all the images in the correct size and format into some folder</li> <li>Open PowerShell</li> <li>Use the following command to load the image into a variable: <ul> <li><code class="language-plaintext highlighter-rouge">$ADphoto = [byte[]](Get-Content "&lt;path to file&gt;" -Encoding byte)</code></li> </ul> </li> <li>Use the following command to put the variable contents into the AD account: <ul> <li><code class="language-plaintext highlighter-rouge">Set-ADUser "&lt;username&gt;" -Replace @{thumbnailPhoto=$ADphoto}</code></li> </ul> </li> <li>You can repeat step 3 followed by 4 for all the users, the variable will be overwritten each time</li> </ol> </li> </ul> <h2 id="testing">Testing</h2> <p>To quickly test if everything works, you can login to any connected client that is in the organisational unit where the group policy applies. Run <code class="language-plaintext highlighter-rouge">gpupdate</code> in the PowerShell and then log out and back in again. The profile picture should be synced correctly.</p> <h2 id="troubleshooting">Troubleshooting</h2> <ul> <li>If the logoff takes a really long time, make sure that you added the script to the <strong>PowerShell Scripts</strong> Tab, not the <strong>Scripts</strong> tab</li> <li>If nothing happens, double check the permissions of the script on disk or the registry key in the group policy</li> <li>Make sure that Windows is activated, as a non activated windows will not show account pictures</li> </ul> <hr /> <p>If you have any improvements or comments, please <a href="https://support.lrk.sh/blog">let me know</a>!</p> ]]> </content> <author> <name>Lerk</name> </author><category term="how-to"/><category term="software"/><category term="windows"/><category term="active-directory"/><category term="customization"/></entry><entry> <title>Customize EngineOS layer colors to whatever you want!</title> <id>https://lerks.blog/p/change-sc6000-layer-colors</id> <updated>2022-12-20T20:00:00+00:00</updated> <link href="https://lerks.blog/p/change-sc6000-layer-colors" rel="alternate" type="text/html" title="Customize EngineOS layer colors to whatever you want!"/><summary type="html"> <![CDATA[<p>DenonDJ is a brand of inMusicBrands that manufactures DJ Hardware and (at least partially) develops the EngineOS and EngineDJ software.</p><p>I bought my first DJ Controller in 2015 and in 2022 upgraded to a set of two SC6000 players and one X1850 mixer. I have been trying to get around the fenced-in software and customize the units to the best of my abilities ever since, and now finally have reached a milestone that I want to share today.</p>]]> </summary><content type="html" xml:base="https://lerks.blog/"> <![CDATA[<p>DenonDJ is a brand of inMusicBrands that manufactures DJ Hardware and (at least partially) develops the EngineOS and EngineDJ software.</p> <p>I bought my first DJ Controller in 2015 and in 2022 upgraded to a set of two SC6000 players and one X1850 mixer. I have been trying to get around the fenced-in software and customize the units to the best of my abilities ever since, and now finally have reached a milestone that I want to share today.</p> <h2 id="how-we-got-there">How we got there</h2> <p>But first things first: How does all that stuff work?</p> <p>The mixer has a LAN hub (or switch, docs are inconclusive) with four ports reserved for players and one port for an external network or a computer. The mixer uses this to get the current BPM value and maybe other stuff from the players and in addition the players can use the connection to access their connected libraries.</p> <p>The SC6000 has one feature that I really like: The ability to insert a SATA hard drive so you don’t have to insert your USB stick every time. When using that internal hard drive to sync, you have to reboot the player into what is called “computer mode”. This shares access to the internal drive to the host computer as a mass storage device which is then used by EngineDJ to sync the library to it.</p> <p>This library consists of the track databases for local and streaming tracks, an optional SoundSwitch project and a “user profile” that contains preset settings for the player, for example which color each layer has.</p> <p><img src="/assets/images/enginedj-2.png" alt="Screenshot of the layer color section in the EngineDJ user profile editor" /></p> <p>The choices you have there are more than limited:</p> <p><img src="/assets/images/enginedj-1.png" alt="Screenshot of the possible layer color choices in the EngineDJ user profile editor" /></p> <p>Luckily, the <code class="language-plaintext highlighter-rouge">EngineLibrary/user.profile</code> file contains the player and layer colors as hex values, better yet: EngineOS actually respects that. Even the X1850 firmware shows whatever color you throw at it!</p> <h2 id="the-user-profile">The user profile</h2> <p>The <code class="language-plaintext highlighter-rouge">user.profile</code> file is a JSON file and as such can be edited with any text editor. Here is the color section for player one:</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"PlayerColor1"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"color"</span><span class="p">:</span><span class="w"> </span><span class="s2">"#ff9000ff"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="mi">16</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"PlayerColor1A"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"color"</span><span class="p">:</span><span class="w"> </span><span class="s2">"#ff9000ff"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="mi">16</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"PlayerColor1B"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"color"</span><span class="p">:</span><span class="w"> </span><span class="s2">"#ff00ffe2"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="mi">16</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div> <p>As you can see, every player has a “base(?)” color and a color for each layer. You can’t change the “base” player color in EngineDJ, only the layer colors. As far as I can tell, the base color is used before anything is selected after bootup and is saved when selecting a user profile (so the first time you start the player with the changed profile the default color will be used, but after selecting the profile, the custom color will reappear after rebooting).</p> <h2 id="color-codes">Color codes</h2> <p>The color codes work a bit like reversed Android Hex color codes, where the first two chars are for transparency (and are <code class="language-plaintext highlighter-rouge">ff</code>/<code class="language-plaintext highlighter-rouge">255</code> by default) and the other six for colors, in the same <code class="language-plaintext highlighter-rouge">rrggbb</code> format that is also used in CSS.</p> <p>So for example the red color in CSS is <code class="language-plaintext highlighter-rouge">#ff0000</code> (so the red channel is at max, the green and blue at min). Add the transparency values to get <code class="language-plaintext highlighter-rouge">#ffff0000</code>, where the fist two characters are the transparency (no transparency/maximum opacity) followed by the six color chars.</p> <p>Now we can get to changing stuff and seeing what we get from it!</p> <h2 id="possibilities">Possibilities</h2> <p>I have recorded a few videos showing what’s possible.</p> <p>For example, here is a purple and teal color combination:</p> <video controls="" class="video-js" data-setup="{ &quot;liveui&quot;: false, &quot;autoplay&quot;: false, &quot;preload&quot;: &quot;auto&quot;, &quot;controlBar&quot;: {&quot;remainingTimeDisplay&quot;: {&quot;displayNegative&quot;: false},&quot;progressControl&quot;: {&quot;seekBar&quot;: true}}, &quot;html5&quot;: {&quot;vhs&quot;: {&quot;enableLowInitialPlaylist&quot;: true,&quot;experimentalBufferBasedABR&quot;: true,&quot;maxPlaylistRetries&quot;: 30}} }" poster="/assets/streams/engine_purple_teal/preview.jpg"> <source src="/assets/streams/engine_purple_teal/stream.m3u8" type="application/vnd.apple.mpegurl" /> <p class="vjs-no-js"> To view this video please enable JavaScript, and consider upgrading to a web browser that <a href="https://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a> </p> </video> <p>But you are not limited to colorful colors, here’s proper purple and black (watch the layer ui, it gets a black border):</p> <video controls="" class="video-js" data-setup="{ &quot;liveui&quot;: false, &quot;autoplay&quot;: false, &quot;preload&quot;: &quot;auto&quot;, &quot;controlBar&quot;: {&quot;remainingTimeDisplay&quot;: {&quot;displayNegative&quot;: false},&quot;progressControl&quot;: {&quot;seekBar&quot;: true}}, &quot;html5&quot;: {&quot;vhs&quot;: {&quot;enableLowInitialPlaylist&quot;: true,&quot;experimentalBufferBasedABR&quot;: true,&quot;maxPlaylistRetries&quot;: 30}} }" poster="/assets/streams/engine_purple_black/preview.jpg"> <source src="/assets/streams/engine_purple_black/stream.m3u8" type="application/vnd.apple.mpegurl" /> <p class="vjs-no-js"> To view this video please enable JavaScript, and consider upgrading to a web browser that <a href="https://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a> </p> </video> <p>And of course you can set the transparency chars to <code class="language-plaintext highlighter-rouge">00</code> to make the color transparent.</p> <p><strong>Although this has a caveat</strong>: It only works in the UI, the jog wheel and layer button will ignore the transparency setting.</p> <p>To get proper transparent layers, you can set the color to transparent black (the ui won’t get a black border):</p> <video controls="" class="video-js" data-setup="{ &quot;liveui&quot;: false, &quot;autoplay&quot;: false, &quot;preload&quot;: &quot;auto&quot;, &quot;controlBar&quot;: {&quot;remainingTimeDisplay&quot;: {&quot;displayNegative&quot;: false},&quot;progressControl&quot;: {&quot;seekBar&quot;: true}}, &quot;html5&quot;: {&quot;vhs&quot;: {&quot;enableLowInitialPlaylist&quot;: true,&quot;experimentalBufferBasedABR&quot;: true,&quot;maxPlaylistRetries&quot;: 30}} }" poster="/assets/streams/engine_purple_transparent/preview.jpg"> <source src="/assets/streams/engine_purple_transparent/stream.m3u8" type="application/vnd.apple.mpegurl" /> <p class="vjs-no-js"> To view this video please enable JavaScript, and consider upgrading to a web browser that <a href="https://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a> </p> </video> <p>Hope this is useful for some 🙂</p> <p><span style="font-size: 11px;">ps: Huge thanks to YouTube for trying to be like TikTok and putting their damn watermark over the videos, even if you download them as uploader.</span></p> <hr /> <p>If you have any improvements or comments, please <a href="https://support.lrk.sh/blog">let me know</a>!</p> ]]> </content> <author> <name>Lerk</name> </author><category term="software"/><category term="hardware"/><category term="denon-dj"/><category term="how-to"/><category term="sc6000"/><category term="x1850"/></entry><entry> <title>Useful git commands and snippets</title> <id>https://lerks.blog/p/useful-git-commands-and-snippets</id> <updated>2021-09-15T22:00:00+00:00</updated> <link href="https://lerks.blog/p/useful-git-commands-and-snippets" rel="alternate" type="text/html" title="Useful git commands and snippets"/><summary type="html"> <![CDATA[<p><a href="https://git-scm.com">Git</a> is probably the most used developer tool (aside from operating systems, shells and <abbr title="nowadays that's basically the same">browsers/editors</abbr>) out there. It can be a lifesaver but if used wrong cause terrible headache. Here are some commands and snippets I’ve encountered and found useful.</p>]]> </summary><content type="html" xml:base="https://lerks.blog/"> <![CDATA[<p><a href="https://git-scm.com">Git</a> is probably the most used developer tool (aside from operating systems, shells and <abbr title="nowadays that's basically the same">browsers/editors</abbr>) out there. It can be a lifesaver but if used wrong cause terrible headache. Here are some commands and snippets I’ve encountered and found useful.</p> <h2 id="prerequisites">Prerequisites</h2> <p>I’m assuming that the default remote in these examples is <code class="language-plaintext highlighter-rouge">origin</code> and <abbr title="any">a</abbr> foreign remote is called <code class="language-plaintext highlighter-rouge">remote</code>. As example remote URL I will either use pseudo local paths, random urls or <a href="https://github.com/torvalds/linux">linux</a> because it has infinite collaborators.</p> <p>The default branch is <code class="language-plaintext highlighter-rouge">master</code> and any other branch is mostly called <code class="language-plaintext highlighter-rouge">branch</code>. The example tag in these examples is in most cases <code class="language-plaintext highlighter-rouge">v1.0.0</code>.</p> <h2 id="git-subcommands">Git Subcommands</h2> <h3 id="git-lfs">git lfs</h3> <p><a href="https://git-lfs.github.com">Git-LFS</a> (Large File Storage) is a git plugin/subcommand that lets you save binary files outside of the git object storage. This saves some space when cloning and works faster. To install it, follow the instructions on the website.</p> <h4 id="repository-setup">Repository setup</h4> <p>Assuming you want to track <code class="language-plaintext highlighter-rouge">.bin</code> files with Git-LFS:</p> <ol> <li>Change into project directory</li> <li>Initialize Git-LFS: <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git lfs <span class="nb">install</span>
</code></pre></div> </div> </li> <li>Tell Git-LFS which files to track: <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git lfs track <span class="s2">"*.bin"</span> 
</code></pre></div> </div> </li> <li>Add the <code class="language-plaintext highlighter-rouge">.gitattributes</code> file and commit: <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git add .gitattributes <span class="o">&amp;&amp;</span> git commit 
</code></pre></div> </div> </li> </ol> <p>Alternatively, you can skip/speed up step three by putting the following in the <code class="language-plaintext highlighter-rouge">.gitattributes</code> file (create it if it’s not there):</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>*.bin filter=lfs diff=lfs merge=lfs -text
</code></pre></div></div> <h3 id="git-log">git log</h3> <p>When run with no arguments, <code class="language-plaintext highlighter-rouge">git log</code> shows a vi/less style commit history output. But it can also be used for more specific stuff.</p> <h4 id="show-log-for-specific-remotebranchtag">Show log for specific remote/branch/tag</h4> <p>The following command can be used to show the history of a specific tag or remote and/or branch:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git log remote/master
</code></pre></div></div> <h4 id="show-log-for-specific-subdirectories">Show log for specific subdirectories</h4> <p>You can use the following command if you’re in a subdirectory of a repository and only want to see the local history:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git log <span class="nt">--</span> ./
</code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">--</code> delimiter is used to make a clear separation between refs and paths. This can also be seen in some git error messages.</p> <h3 id="git-tag">git tag</h3> <p>A tag is like a bookmark on a specific commit. Mostly it’s a good practice to tag the version bump commits (if those are made).</p> <h4 id="listing-tags">Listing tags</h4> <p>While git tags can be seen as an appendage next to the commit id in <code class="language-plaintext highlighter-rouge">git log</code>, it’s also possible to show all available tags.</p> <h5 id="local-tags">Local tags</h5> <p>I think the command below is self-explanatory:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git tag <span class="nt">-l</span>
</code></pre></div></div> <h5 id="remote-tags">Remote tags</h5> <p>Listing remote tags can either be done for the default remote:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git ls-remote <span class="nt">--tags</span>
</code></pre></div></div> <p>Or a specific remote:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git ls-remote <span class="nt">--tags</span> remote
</code></pre></div></div> <h4 id="creating-tags">Creating tags</h4> <p>A tag can be created like this:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git tag v1.0.0
</code></pre></div></div> <p>The tag will now show up in the git history as <code class="language-plaintext highlighter-rouge">v1.0.0</code> which is an often used scheme. Btw have you heard of <a href="https://semver.org/">semver</a>?</p> <h4 id="pushing-tags">Pushing tags</h4> <p>Since <code class="language-plaintext highlighter-rouge">git push</code> doesn’t submit patches by default, a flag is needed:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git push <span class="nt">--tags</span>
</code></pre></div></div> <h4 id="deleting-tags">Deleting tags</h4> <p>Deleting tags can be necessary when doing debug stuff (like CI that <em>should</em> build on every tag but there’s a bug in a script of a dependency that only runs on CI so you end up with something like <code class="language-plaintext highlighter-rouge">v1.0.0.a.a.a.a.a.a.a.a.a.a.h.e.l.p</code>).</p> <h5 id="local-tags-1">Local tags</h5> <p>Local tags can be deleted quite easily:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git tag <span class="nt">-d</span> v1.0.0
</code></pre></div></div> <h5 id="remote-tags-1">Remote tags</h5> <p>When removing remote tags, it’s required to to a push:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git push remote :v1.0.0
</code></pre></div></div> <p>You can see more info on this syntax in the section about <code class="language-plaintext highlighter-rouge">git push</code> below.</p> <p><strong>Update</strong>: Since git 1.8.0, you can also use the following which is a bit more readable:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git push <span class="nt">--delete</span> remote v1.0.0
</code></pre></div></div> <h3 id="git-push">git push</h3> <p>The <code class="language-plaintext highlighter-rouge">push</code> command allows to submit changes to a remote. When used without arguments in a properly set up repository, it will push all new (for the remote) commits on the current branch (or it’s remote counterpart) to the default remote.</p> <h4 id="setting-a-default-remote">Setting a default remote</h4> <p>A default (upstream) remote can be set by running the following:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git push <span class="nt">-u</span> remote branch
</code></pre></div></div> <p>This sets the foreign remote called <code class="language-plaintext highlighter-rouge">remote</code> as upstream that will be used by default when running <code class="language-plaintext highlighter-rouge">push</code> or <code class="language-plaintext highlighter-rouge">pull</code>. The <code class="language-plaintext highlighter-rouge">-u</code> flag can be extended to <code class="language-plaintext highlighter-rouge">--set-upstream</code>.</p> <h3 id="git-remote">git remote</h3> <p>A remote is in most cases a collaboration server like <a href="https://about.gitlab.com">GitLab</a>, <a href="https://github.com">GitHub</a> or <a href="https://gitea.io">Gitea</a>, but it can in general be any local (or local-ish mounted) or remotely (ssh/http(s)) available <code class="language-plaintext highlighter-rouge">.git</code> folder.</p> <h4 id="showing-available-remotes">Showing available remotes</h4> <p>All available remotes can be printed out by running:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git remote <span class="nt">-v</span>
</code></pre></div></div> <h4 id="adding-a-remote">Adding a remote</h4> <p>Adding a remote can be done by running the following:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git remote add remote /path/to/repo/.git
</code></pre></div></div> <p>When adding a remote remote (haha), a protocol and/or authentication might be required:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git remote add remote ssh://git@example.com:foo/bar.git
</code></pre></div></div> <h4 id="deleting-a-remote">Deleting a remote</h4> <p>Deleting a remote can be done like this:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git remote remove remote
</code></pre></div></div> <h4 id="changing-a-remotes-url">Changing a remote’s URL</h4> <p>When a repository was moved or there is some other stuff going on, a remote url can be changed by running the following:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git remote set-url remote /path/to/new/repo/.git
</code></pre></div></div> <p>Note that by default, only the fetch url is updated by this command. It’s also default for git to use only the fetch url when a push url is not explicitly set.<br /> In case of local mirrors (see section about <code class="language-plaintext highlighter-rouge">git clone</code> below) it might be required to set the push url to the original remote. This can be done by appending the <code class="language-plaintext highlighter-rouge">--push</code> flag after <code class="language-plaintext highlighter-rouge">set-url</code> (and before the url).</p> <h3 id="git-clone">git clone</h3> <p>Cloning means downloading/synchronizing a remote repository to/with your machine. It’s used to get an initial copy of the repository (updating is done with <code class="language-plaintext highlighter-rouge">git pull</code> instead).</p> <h4 id="cloning-in-general">Cloning in general</h4> <p>Most Git platforms mentioned above support cloning by passing the URL of the repository’s web frontend like the following:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/torvalds/linux
</code></pre></div></div> <p>This will result in the repository being cloned into the current folder as <code class="language-plaintext highlighter-rouge">linux</code>. On some platforms, the URL might look more like <code class="language-plaintext highlighter-rouge">https://github.com/torvalds/linux.git</code>. In this case, git will omit the <code class="language-plaintext highlighter-rouge">.git</code> extension for the local folder.</p> <h5 id="changing-the-output-directory">Changing the output directory</h5> <p>To clone a remote repository into a folder other than the repository name, you can simply append it. If it does not exist, git will create it automatically. Example:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone /path/to/repo/.git ../otherName
</code></pre></div></div> <p>The command above will result in the repository being cloned into the parent of the current folder as <code class="language-plaintext highlighter-rouge">otherName</code>. You can also specify an absolute path.</p> <h4 id="cloning-as-mirror">Cloning as mirror</h4> <p>In some cases it might be wise to store an exact copy of a remote repository on another place. For instance when the connection to a remote repository is pretty slow or the network has a limited transmission rate.</p> <p>It can be a good idea to clone a <em>very</em> remote repository onto a more local server and let local clients pull from there.<br /> The remote repository can be cloned as mirror by appending the <code class="language-plaintext highlighter-rouge">--mirror</code> flag:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone <span class="nt">--mirror</span> ssh://user@host:repo.git
</code></pre></div></div> <p>The mirror can be kept up to date by running <code class="language-plaintext highlighter-rouge">git remote update</code>. This can be intervalled by using something like the following crontab which will update the mirror every fifteen minutes:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>*/15 * * * * cd /path/to/mirror &amp;&amp; git remote update &gt;/dev/null 2&gt;&amp;1
</code></pre></div></div> <h2 id="advanced-stuff">Advanced Stuff</h2> <p>Below is more advanced stuff. Some of it requires additional software.</p> <h3 id="cleaning-up-messy-contributor-stats">Cleaning up messy contributor stats</h3> <p>We all probably had to deal with this at some point: The committer you use in your git hoster is not the same you use locally and suddenly you appear as three different people because you made commits on your local machine, the web ui and maybe some dev system.</p> <p>You can clean this up by rewriting the git history and changing the duplicated committers to use your desired data. <strong>Note that this requires a force push and will open a whole new can of worms when lots of people use the repo!</strong></p> <p>First install <a href="https://github.com/newren/git-filter-repo"><code class="language-plaintext highlighter-rouge">git-filter-repo</code></a>, and then modify the following command to suit your setup (the callback is python code):</p> <p><em>The <code class="language-plaintext highlighter-rouge">bytearray("".encode())</code> at the <code class="language-plaintext highlighter-rouge">new_name</code> variable is needed in case the committer name you want to use contains special characters. You could also supply a plain string if your name doesn’t contain special chars.</em></p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git filter-repo <span class="nt">--commit-callback</span> <span class="s1">'                                                                       ⬡ 16.15.0
old_email = b"duplicated_committer@example.com"
new_email = b"real_committer@example.com"
new_name =  bytearray("New Committer Name".encode())

if commit.author_email == old_email:
    commit.author_email = new_email
    commit.author_name = new_name

if commit.committer_email == old_email:
    commit.committer_email = new_email
    commit.committer_name = new_name
'</span>
</code></pre></div></div> <p><strong>Note:</strong> <code class="language-plaintext highlighter-rouge">git-filter-repo</code> will complain when not run on a fresh clone, and will delete your remotes, so you don’t accidentally do weird stuff. You’ll need to re-add them in order to force push after the correction.</p> <hr /> <p>If you have any improvements or comments, please <a href="https://support.lrk.sh/blog">let me know</a>!</p> ]]> </content> <author> <name>Lerk</name> </author><category term="snippets"/><category term="software"/><category term="git"/></entry><entry> <title>How to flash Marlin to your Anet A8 3D Printer</title> <id>https://lerks.blog/p/how-to-flash-marlin-to-your-anet-a8-3d-printer</id> <updated>2021-08-18T16:24:35+00:00</updated> <link href="https://lerks.blog/p/how-to-flash-marlin-to-your-anet-a8-3d-printer" rel="alternate" type="text/html" title="How to flash Marlin to your Anet A8 3D Printer"/><summary type="html"> <![CDATA[<p>The hardware might already be a bit old but I recently got an Anet A8 3D Printer. In this post I’ll describe how to flash a current version of the Marlin firmware that offers a lot of additional features and basic protection stuff that Anet was too lazy (?) to add to the stock firmware.</p>]]> </summary><content type="html" xml:base="https://lerks.blog/"> <![CDATA[<p>The hardware might already be a bit old but I recently got an Anet A8 3D Printer. In this post I’ll describe how to flash a current version of the Marlin firmware that offers a lot of additional features and basic protection stuff that Anet was too lazy (?) to add to the stock firmware.</p> <p>I got the printer from a friend, so the thing was already fully assembled, used and modified a few times with a few broken cables and loose screws. It still was on some version of the stock firmware and called itself “Omni A8” when booting, hinting to the firmware being quite old.</p> <p>After watching a lot of different videos and reading probably all of the available blog posts I could find, I settled on a mashup of the methods that were presented because this was the most comfortable way of doing this for me.</p> <h2 id="requirements">Requirements</h2> <p>There are a few things we’ll need to do before the flashing can begin.</p> <h3 id="programmer">Programmer</h3> <p>From the hardware point of view, the A8 alone won’t be enough.</p> <p>You’ll also need the USBasp flasher. Either for flashing a bootloader that allows you to flash a firmware using the USB port on the printer board or for flashing the firmware directly.</p> <p>I ordered <a href="https://www.amazon.de/dp/B08867WS62/">this one</a> mainly because it was relatively cheap and came in a two pack, which might be <em>important later</em> if you get the cheap ones.</p> <h3 id="vs-code">VS Code</h3> <p>Yes, I know it’s garbageware by garbagecorp but I had this installed already for ESP32 stuff before I switched to PlatformIO in CLion <strong>and</strong> there is a VSC plugin that provides one-click builds of Marlin.</p> <p>You can download that thing <a href="https://code.visualstudio.com">here</a>.</p> <h4 id="plugins">Plugins</h4> <p>Once downloaded and started for the first time, click on the “Plugins” tab (the four cubes where the top right cube is offset) and install the <a href="https://marketplace.visualstudio.com/items?itemName=platformio.platformio-ide">PlatformIO IDE</a> plugin.</p> <p>After PlatformIO has finished installing (you’ll need to reload the webpage that is the editor) you’ll need to install the <a href="https://marketplace.visualstudio.com/items?itemName=MarlinFirmware.auto-build">Auto Build Marlin</a> plugin.</p> <h3 id="avrdude">AVRDUDE</h3> <p>Another required software is <a href="https://www.nongnu.org/avrdude/">AVRDUDE</a>. On macOS this can be installed with <a href="https://brew.sh/">Homebrew</a>, on Linux you might have it in your distributions package repository.</p> <h2 id="preparations">Preparations</h2> <p>This section will mostly prevent you from running into problems later.</p> <h3 id="updating-the-usbasp">Updating the USBasp</h3> <p>The first issue I had with all the available guides was that the firmware on the USBasp was too old. This issue will make any upload attempts fail with some sort of sync error.</p> <p>That’s why it’s good that the flasher I bought came in a two pack, because to update the firmware you’ll need a second one.</p> <p>If you think that this is already a bit ridiculous, keep in mind that the latest firmware for this thing was released in 2011! 🙈</p> <p>To allow the firmware upgrade, you’ll need to find two contacts on the USBasp you want to update labeled <code class="language-plaintext highlighter-rouge">J2</code> and bridge them. If you’re lucky those contacts might have pins soldered on them, in my case there were just the contacts so I bridged them with a bit of solder.</p> <p>Additionally, the jumpers on both flashers must be set to <strong>5V for both of them</strong>.</p> <p>If the all the jumpers are set to the required positions, plug in the cable into both flashers (feels obvious but <em>align the notches, don’t force it the other way</em>) and plug the flasher where J2 is <strong>not bridged</strong> into your computers USB port.</p> <p>You can find the latest version of the firmware <a href="https://www.fischl.de/usbasp/">on this page</a> as of the time this post is written the file to download is called <code class="language-plaintext highlighter-rouge">usbasp.2011-05-28.tar.gz</code>.</p> <p>Extract the contents of this file, then create a new folder somewhere and copy the file <code class="language-plaintext highlighter-rouge">/bin/firmware/usbasp.atmega8.&lt;VERSION&gt;.hex</code> from the extracted files into the new folder. Replace <code class="language-plaintext highlighter-rouge">&lt;VERSION&gt;</code> with the version of the firmware you downloaded.</p> <p>Now open a terminal, change into the previously created folder and execute the following command to create a backup of the current firmware on your USBasp:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>avrdude <span class="nt">-c</span> usbasp <span class="nt">-p</span> atmega8 <span class="nt">-U</span> flash:r:usbasp-backup.bin:r
</code></pre></div></div> <p>This will create a file called <code class="language-plaintext highlighter-rouge">usbasp-backup.bin</code> in the current folder.</p> <p>The next step is to upload the new firmware, again replace <code class="language-plaintext highlighter-rouge">&lt;VERSION&gt;</code> with the version you downloaded:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>avrdude <span class="nt">-c</span> usbasp <span class="nt">-p</span> atmega8 <span class="nt">-U</span> flash:w:usbasp.atmega8.&lt;VERSION&gt;.hex
</code></pre></div></div> <p>This will upload the new firmware and then verify that the written data is correct.</p> <p>If everything went well you can remove the flashers from your computer, remove the cable from the updated unit and then <strong>remove the J2 bridge</strong> to disable update mode.</p> <h3 id="downloading-marlin">Downloading Marlin</h3> <p>Now we can finally start with the actual printer firmware. First of all you’ll need the firmware code itself.</p> <p>You can find the latest version on <a href="https://github.com/MarlinFirmware/Marlin/releases">the GitHub releases page</a>. In my case the current version was <strong>2.0.9.1</strong> (this will be important later).</p> <p>At the end of each release is a list of files, we’ll need the archive of the source code. You can extract it you want, but you need to extract it.</p> <p>You’ll also need the configuration files, which are stored in a separate repo.<br /> You can find an overview of the available branches on <a href="https://github.com/MarlinFirmware/Configurations/branches">this page</a>, if you downloaded the latest version you can probably find the correct branch in the “Active branches” box.</p> <p>When clicking on the branch name (which should be <code class="language-plaintext highlighter-rouge">release-&lt;VERSION&gt;</code>, where <code class="language-plaintext highlighter-rouge">&lt;VERSION&gt;</code> is the version of Marlin you downloaded), you can use the “Code” dropdown to download a ZIP of the corresponding branch.</p> <p>Extract the configurations anywhere you want, then navigate to <code class="language-plaintext highlighter-rouge">/config/examples/Anet/A8</code> and copy all the files into the <code class="language-plaintext highlighter-rouge">Marlin</code> folder inside the extracted Marlin source code (there should be file called <code class="language-plaintext highlighter-rouge">Version.h</code> in the same directory), replace any conflicting files!</p> <h2 id="configuring-the-firmware">Configuring the firmware</h2> <p>Now that all the necessary files are downloaded, extracted and put in the right place, we can start configuring the firmware.</p> <p>To do this, open the Marlin root folder (where the <code class="language-plaintext highlighter-rouge">README.md</code> is in) with VS Code using the “Open Folder” button.<br /> You might also need to confirm that you trust the project for all the features to work properly.</p> <p>Then open <code class="language-plaintext highlighter-rouge">/Marlin/Configuration.h</code>.</p> <h3 id="thermal-runaway-protection">Thermal Runaway Protection</h3> <p>The first feature that you should enable is thermal runaway protection. This will make the firmware make sure that the temperature measured is rising when a heater is active.<br /> So in case of a faulty, not connected or fallen off sensor the heating stops (instead of getting hotter until everything starts burning).</p> <p>With the <code class="language-plaintext highlighter-rouge">Configuration.h</code> opened, press <kbd>cmd+f</kbd> or <kbd>ctrl+f</kbd> to open the search box and search for <code class="language-plaintext highlighter-rouge">THERMAL_PROTECTION</code>.</p> <p>As of version 2.0.9.1, there are four lines starting with <code class="language-plaintext highlighter-rouge">#define</code>, enabling the protection for <code class="language-plaintext highlighter-rouge">HOTENDS</code>, <code class="language-plaintext highlighter-rouge">BED</code>, <code class="language-plaintext highlighter-rouge">CHAMBER</code> and <code class="language-plaintext highlighter-rouge">COOLER</code>. All of these should be enabled (VS Code will color the defines pink and the names blue instead of coloring everything green and the line starts with <code class="language-plaintext highlighter-rouge">#define</code>, not with <code class="language-plaintext highlighter-rouge">//#define</code>.</p> <h3 id="mesh-bed-leveling">Mesh Bed Leveling</h3> <p>Another useful feature is mesh bed leveling which allows you to manually configure zero points (or more precisely their offsets) for nine points on the bed. The printer will then use the resulting offsets to nicely print on a not perfectly leveled bed.</p> <p>To enable this, search for <code class="language-plaintext highlighter-rouge">MESH_BED_LEVELING</code> and make sure the feature is defined.</p> <h4 id="leveling-menu-items">Leveling Menu Items</h4> <p>To have a menu available in the firmware that will let you level manually, you need to enable the define <code class="language-plaintext highlighter-rouge">LEVEL_BED_CORNERS</code>. A few lines below that you can also enable <code class="language-plaintext highlighter-rouge">LEVEL_CENTER_TOO</code> if you want a more precise leveling.</p> <h3 id="stepper-inverts">Stepper Inverts</h3> <p>Since the printer I got came with a customized extruder, the direction happened to be reversed. The first time I tried to extrude, the filament came out at the top.</p> <p>This can be fixed by searching for <code class="language-plaintext highlighter-rouge">INVERT_E0_DIR</code> and setting this to <code class="language-plaintext highlighter-rouge">true</code> to make the firmware change directions.</p> <p>This might not be needed if you have the stock A8 extruder!</p> <p>And in case you have other modified motors you can find the corresponding defines for all the motors in the same area of the file.</p> <h2 id="building-the-firmware">Building the firmware</h2> <p>Now that all the needed configuration is done the firmware needs to be built. This can be done by opening the “Auto Build Marlin” Tab (the one with the <code class="language-plaintext highlighter-rouge">m</code>).</p> <p>In the opened side menu, click the hammer symbol at the top:</p> <p><img src="/assets/images/anet-marlin-1.png" alt="Screenshot of the Auto Build Marlin toolbar" /></p> <p>This will open the build tab where you can click the build button for the <code class="language-plaintext highlighter-rouge">sanguino1284p</code> environment to start the firmware build:</p> <p><img src="/assets/images/anet-marlin-2.png" alt="Screenshot of the build section" /></p> <p>When the build is finished successfully, the terminal will show output like the following:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Building in release mode
Checking size .pio/build/sanguino1284p/firmware.elf
Advanced Memory Usage is available via "PlatformIO Home &gt; Project Inspect"
RAM:   [===       ]  26.3% (used 4307 bytes from 16384 bytes)
Flash: [========= ]  93.2% (used 118336 bytes from 126976 bytes)
================================================= [SUCCESS] Took 2.95 seconds =================================================

Environment    Status    Duration
-------------  --------  ------------
sanguino1284p  SUCCESS   00:00:02.950
================================================= 1 succeeded in 00:00:02.950 =================================================
</code></pre></div></div> <p>You can see the amount of flash that is left over is pretty small. If we start enabling more features we might run out of space.</p> <p>That is why I decided to not flash any bootloader and simply flash the firmware directly everytime I need to flash a new one.</p> <p>A successful build will also show a success message:</p> <p><img src="/assets/images/anet-marlin-3.png" alt="Screenshot of the build section with a finished build" /></p> <p>You can click the folder icon to open the output folder. A file called <code class="language-plaintext highlighter-rouge">firmware.hex</code> should be selected as well.</p> <p>Copy this file somewhere where you’ll find it again and maybe give it a more descriptive name (I like to add the features I enabled and the date to the name).</p> <h2 id="flashing-the-firmware">Flashing the firmware</h2> <p>Now it’s time to flash the compiled firmware to the printer.<br /> To do this, first find the USBasp that has the current firmware on it and make sure that <code class="language-plaintext highlighter-rouge">J2</code> is <strong>not</strong> bridged and that the power is set to <strong>5V</strong>.</p> <p>Connect the cable to the USBasp and the included 6 pin female adapter (see picture below) to the other side of the cable.</p> <p><img src="/assets/images/anet-marlin-4.png" alt="Photo of the USBasp 6 pin female adapter" /></p> <p><strong>IMPORTANT:</strong> Before continuing <strong>disconnect the power supply</strong> from the printer. Make sure the printer is <strong>powered off</strong> and <strong>not plugged in</strong>.</p> <p>The 6 pin adapter needs to be connected to the middle pins of the JTAG connector on the printer board. The side with the <code class="language-plaintext highlighter-rouge">RST</code> pin must face the notch in the socket on the printer board. See the diagram below for reference:</p> <p><img src="/assets/images/anet-marlin-5.jpg" alt="Photo of the flash connector on the Anet A8 board" /><br /> <span style="font-size: 11px;">This picture is licensed CC-BY-SA by <a href="https://3dprint.wiki/reprap/anet/electronics/mainboard">3dprint.wiki</a>.</span></p> <p>If everything is connected, plug the USBasp into your computer. <strong>DO NOT turn the printer on! Leave it disconnected!</strong> The printer board will receive power from the USBasp (or more precisely your computer).</p> <p>Now navigate to the folder with the compiled firmware and start by pulling a backup from the printer:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>avrdude <span class="nt">-c</span> usbasp <span class="nt">-p</span> atmega1284p <span class="nt">-U</span> flash:r:flashbackup-anet-a8.bin:r
</code></pre></div></div> <p>This will create a file called <code class="language-plaintext highlighter-rouge">flashbackup-anet-a8.bin</code> containing the original firmware on the printer.</p> <p>Now we can flash the Marlin version that was compiled earlier, replace <code class="language-plaintext highlighter-rouge">firmware.hex</code> with the name you chose for the compiled firmware:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>avrdude <span class="nt">-c</span> usbasp <span class="nt">-p</span> atmega1284p <span class="nt">-U</span> flash:w:firmware.hex
</code></pre></div></div> <p>This will flash the firmware to the printer and then read it to verify that the data written to the printer is correct.</p> <p>If the flashing is complete, unplug the USBasp from your computer and then remove it from the printer board, afterwards you can plug in the printer again and test if everything works.</p> <h2 id="final-thoughts">Final thoughts</h2> <p>After the first boot, you’ll probably see an EEPROM error, this is because the memory gets cleared by avrdude everytime a new firmware is written. You can select <code class="language-plaintext highlighter-rouge">reset</code> and press the middle button to initialize the memory.</p> <p>I know that connecting the USBasp every time you make a change in the firmware configuration is not as comfortable as just plugging in the USB cable but I’d rather have as much space as possible available to the firmware in case I add more stuff to the printer later on.</p> <hr /> <p>I hope this guide was useful to you, if you have any problems or additions <a href="https://support.lrk.sh/blog">feel free to contact me</a>.</p> ]]> </content> <author> <name>Lerk</name> </author><category term="how-to"/><category term="software"/><category term="hardware"/><category term="marlin"/><category term="anet-a8"/><category term="3d-printing"/></entry><entry> <title>Secure initial setup of the ODROID GO Advance</title> <id>https://lerks.blog/p/secure-initial-setup-of-the-odroid-go</id> <updated>2021-05-16T03:49:23+00:00</updated> <link href="https://lerks.blog/p/secure-initial-setup-of-the-odroid-go" rel="alternate" type="text/html" title="Secure initial setup of the ODROID GO Advance"/><summary type="html"> <![CDATA[<p>In this post I’ll describe how to secure the default installation of the ODROID GO Advance. This guide may also work on other Linux Handhelds with some modifications.</p>]]> </summary><content type="html" xml:base="https://lerks.blog/"> <![CDATA[<p>In this post I’ll describe how to secure the default installation of the ODROID GO Advance. This guide may also work on other Linux Handhelds with some modifications.</p> <p>For completing the guide you’ll need to know your way around a linux shell and its editors. I’ll install <code class="language-plaintext highlighter-rouge">vim-nox</code> during this tutorial but of course you can use the already installed <code class="language-plaintext highlighter-rouge">nano</code> editor or install a different editor of your choice.</p> <h2 id="odroid-go-advance">ODROID GO Advance</h2> <p>The <a href="https://wiki.odroid.com/odroid_go_advance/start">ODROID GO Advance</a> is a handheld console shaped similar to a GameBoy Advance.<br /> The device comes with a quad-core Cortex-A35 CPU, 1GB DDR3 RAM and a 320×480 LCD screen. ODROID also provides <a href="https://wiki.odroid.com/odroid_go_advance/os_image/ubuntu_es">an Ubuntu based operating system</a> which I’ll be using for this guide.</p> <h2 id="the-initial-setup">The initial setup</h2> <p>This part of the guide will probably be completely different if you use a different device. I’d recommend reading it as well, maybe there are still some helpful bits.</p> <h3 id="creating-the-microsd-card">Creating the microSD card</h3> <p>The first step in setting up the ODROID GO Advance is to <a href="https://wiki.odroid.com/odroid_go_advance/make_sd_card">flash the microSD card</a>.<br /> Regarding the SD card, I have tested two cards I had around but some games were slower than the device specs would suggested so I bought a more expensive card with a <a href="https://www.sdcard.org/developers/sd-standard-overview/speed-class/">higher speed class</a> and some games now work a lot better.</p> <p>When booting the ODROID for the first time after flashing the microSD card, the device will expand the flashed file system to use all the available space, therefore the boot might take a bit longer. You’ll also probably see the pink Ubuntu splash screen which we’ll disable later on so booting and switching applications looks more consistent.</p> <h3 id="setting-up-wifi">Setting up WiFI</h3> <p>After the device booted the first thing to do is to connect to a wireless network. The ODROID Go Advance has an ESP32 soldered to the board that acts as the network module. It’s not the fastest and doesn’t support modern (5GHz, WPA3) networks so you might have to fiddle with your <abbr title="Wireless Access Point">AP</abbr> settings if you can’t find your network.</p> <p>Connecting to a new network can be done by pressing the <kbd>A</kbd> button while the “configuration” menu item is selected, then using the D-Pad to select “WiFi” and <kbd>A</kbd> to open the network management application.</p> <p>There might already be a primary network defined which will be selected. Changing selection to the <code class="language-plaintext highlighter-rouge">+</code> button can be done by pressing the (outer) shoulder buttons. Selecting is still done with <kbd>A</kbd>.</p> <p>There should now be a list with available networks. Select the network you want to connect to and a dialog box asking for the password should show up. There is a bit of a bug in this dialog because aborting with the <code class="language-plaintext highlighter-rouge">x</code> at the bottom left corner will still add the network which has to be deleted manually.</p> <p>After putting in the password and confirming with the checkmark button, there should be a “play icon” next to the network name to show that it’s connected. To go back to the main menu, the <code class="language-plaintext highlighter-rouge">x</code> button has to be selected using the shoulder buttons.</p> <h3 id="connecting-using-ssh">Connecting using SSH</h3> <p>To easily find the IP of the device, open the “network info” application from the configuration menu. Then connect to the device using the username and password <code class="language-plaintext highlighter-rouge">odroid</code> (replace <code class="language-plaintext highlighter-rouge">&lt;deviceIP&gt;</code> with the IP address):</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh odroid@&lt;deviceIP&gt;
</code></pre></div></div> <h3 id="installing-utilities">Installing utilities</h3> <p>To finish the initial setup, let’s update the system and install some useful utilities:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt update <span class="o">&amp;&amp;</span> <span class="nb">sudo </span>apt <span class="nt">-y</span> upgrade <span class="o">&amp;&amp;</span> <span class="nb">sudo </span>apt <span class="nt">-y</span> <span class="nb">install </span>vim-nox htop git
</code></pre></div></div> <h2 id="securing-the-device">Securing the device</h2> <p>Now let’s continue with securing the device, starting with the most obvious thing.</p> <h3 id="changing-the-passwords">Changing the password(s)</h3> <p>To prevent other people from connecting to your device, change the password for the <code class="language-plaintext highlighter-rouge">odroid</code> user:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>passwd
</code></pre></div></div> <p>Remember that this user also has <code class="language-plaintext highlighter-rouge">sudo</code> capabilities so the <code class="language-plaintext highlighter-rouge">root</code> account is basically just as secure as the <code class="language-plaintext highlighter-rouge">odroid</code> account!</p> <h3 id="securing-ssh">Securing SSH</h3> <p>To make remote login even harder, SSH can be configured to only allow login for non-root users and only with key based authentication. This can be done by editing <code class="language-plaintext highlighter-rouge">/etc/ssh/sshd_config</code> to make it contain the following values:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PermitRootLogin no
PasswordAuthentication no
</code></pre></div></div> <h4 id="fail2ban">fail2ban</h4> <p>Although it might be a bit overkill on smaller networks, when you take your device with you and connect to some sort of large or open(ly connected to the internet) network, some people (or bots) might try to brute force your SSH.</p> <p>To make their life harder, you can install fail2ban:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> fail2ban
</code></pre></div></div> <p>A description on how to configure fail2ban can be found in my <a href="/p/how-to-securely-set-up-a-new-server#fail2ban">post about setting up servers</a></p> <h3 id="installing-a-firewall">Installing a firewall</h3> <p>Installing a firewall is a good idea if you don’t plan on using the “Playertoo” feature of the ODROID and plan to use the device in non-trusted networks and/or with non-trusted software. Of course the probability of some software opening unwanted ports on the ODROID is pretty low, but it’s never zero and the firewall doesn’t use that much energy.</p> <p><a href="https://wiki.ubuntu.com/UncomplicatedFirewall">UFW</a> is a pretty easy to use IPTables based firewall script, it can be installed by executing:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nt">-y</span> <span class="nb">install </span>ufw
</code></pre></div></div> <p>Before activating the firewall, we’ll need to make sure to allow SSH access to not lock ourselves out:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>ufw allow ssh
</code></pre></div></div> <p>Then the firewall can be activated:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>ufw <span class="nb">enable</span>
</code></pre></div></div> <p>This command will warn about possible disruption of SSH connections which has to be confirmed by entering <code class="language-plaintext highlighter-rouge">y</code>. After this, the command should finish with <code class="language-plaintext highlighter-rouge">Firewall is active and enabled on system startup</code>.</p> <p>Now let’s check the firewall status:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>ufw status
</code></pre></div></div> <p>You’ll see that currently only SSH (port 22) is allowed for IPv4 and v6. More rules can be added by executing <code class="language-plaintext highlighter-rouge">ufw allow</code> followed by the port number or service name.</p> <p>For my setup, SSH is enough.</p> <h3 id="logwatch">Logwatch</h3> <p>If you want to go really overboard, you can also install logwatch to receive periodic log analytics via email. This requires that you save the credentials to the mail account on the unencrypted SD card “anyone” could access easily so this also opens other holes.</p> <p>You can find the instructions <a href="/p/how-to-securely-set-up-a-new-server#logwatch">in my server setup post</a>. The only thing that should not be followed in those instructions is creating the cronjobs for reasons I describe below. The deletion of the cronjob installed by APT should still be done!</p> <p>The problem with the cronjobs is that they won’t be executed if the ODROID is not running. Since I want the log analytics only once a week, the probability of the system not being turned on during the cronjob trigger is very high. To mitigate this, I start the cronjob using <code class="language-plaintext highlighter-rouge">anacron</code> which will execute the cronjob on system boot if an execution was due while the system was off.</p> <p>This can be done by adding the following content to the <code class="language-plaintext highlighter-rouge">/etc/anacrontab</code> file:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># days	delay	identifier	command
  7	    2	    logwatch	/usr/sbin/logwatch
</code></pre></div></div> <p>The delay in the config will give the device (2 minutes) time to connect to a wireless network after boot.</p> <h3 id="smb">SMB</h3> <p>By default there is also an SMB service running (which will be blocked by the firewall in case it it active). I don’t consider SMB as secure so I disabled it using:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl stop smbd <span class="o">&amp;&amp;</span> <span class="nb">sudo </span>systemctl disable smbd
</code></pre></div></div> <h2 id="final-tweaks">Final tweaks</h2> <p>The next few things are not really focused on security, more on the convenience of using the device.</p> <h3 id="removing-ubuntu-advantage">Removing Ubuntu Advantage</h3> <p>Ubuntu Advantage is some Enterprise management software which doesn’t makes sense on a handheld device. It can be removed by running:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt purge ubuntu-advantage-tools
</code></pre></div></div> <h3 id="disabling-the-ubuntu-spash-screen">Disabling the ubuntu spash screen</h3> <p>When booting or shutting down the system and when changing emulators, there is a purple Ubuntu splash screen shown.</p> <p>It can be turned off by changing the arguments given to the kernel at boot time. For this, we’ll first need mount the boot partition:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>mount /dev/mmcblk0p1 /mnt
</code></pre></div></div> <p>Now the file <code class="language-plaintext highlighter-rouge">/mnt/boot.ini</code> can be edited. In this file, in the line starting with <code class="language-plaintext highlighter-rouge">setenv bootargs</code>, there are the arguments <code class="language-plaintext highlighter-rouge">quiet splash</code>.</p> <p>When <code class="language-plaintext highlighter-rouge">splash</code> is removed from that line, the purple splash screen is hidden. When <code class="language-plaintext highlighter-rouge">quiet</code> is removed, the system will show log messages at boot.</p> <p>After the changes are made, the boot partition can be unmounted and synced:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>umount /mnt <span class="o">&amp;&amp;</span> <span class="nb">sync</span>
</code></pre></div></div> <p>After a reboot, the splash and/or the messages will be shown depending on the changes you’ve made.</p> <h3 id="transferring-roms">Transferring ROMs</h3> <p>While transferring roms <em>can</em> be done using SCP or SMB, I would recommend to do this using a microSD Adapter (and a Linux host that can read extFS) or inserting (and mounting) a USB Stick into the ODROID because the WiFi Adapter (an ESP32) is pretty slow.</p> <p>Transferring using a microSD Adapter on my ThinkPad resulted in a 5x faster transfer compared to using WiFi.</p> <h3 id="backups">Backups</h3> <p>Since ODROID releases any updates as a new <code class="language-plaintext highlighter-rouge">.img</code> file rather than simply using the package manager already present on the system, I’d recommend only backing up the <code class="language-plaintext highlighter-rouge">/roms</code> and the <code class="language-plaintext highlighter-rouge">/home/odroid</code> folder.</p> <p>This of course means that this guide has to be walked through again after each update. In my opinion it’s still worth it because you never know what got changed with the update. And yes, you could look, but (from my point of view) walking through this guide again is faster than inspecting the new <code class="language-plaintext highlighter-rouge">.img</code> and comparing it to the current version on the SD card.</p> <h2 id="finishing-the-setup">Finishing the setup</h2> <p>Now that the device is fully configured, the last step missing is to reboot:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>shutdown <span class="nt">-r</span> now
</code></pre></div></div> <p>That’s all for now. If you have any more suggestions to make or experience problems, don’t hesitate to write me, I’ll update this port accordingly!</p> <p>I hope this was useful, have a nice day!</p> <hr /> <p>If you have any improvements or comments, please <a href="https://support.lrk.sh/blog">let me know</a>!</p> ]]> </content> <author> <name>Lerk</name> </author><category term="how-to"/><category term="software"/><category term="linux"/><category term="odroid"/><category term="go-advance"/><category term="security"/></entry></feed>