<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="/feeds.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://chameth.com/">
    <title>Chameth.com - posts like artisanal-docker-images, filament-weight-display, why-you-should-be-using-https but not debugging-beyond-the-debugger</title>
    <subtitle>Personal homepage of Chris Smith</subtitle>
    <link href="https://chameth.com/feeds/posts/like/artisanal-docker-images,filament-weight-display,why-you-should-be-using-https/unlike/debugging-beyond-the-debugger/" rel="self"/>
    <link href="https://chameth.com/"/>
    <icon>https://chameth.com/favicon.png</icon>
    <updated>2026-04-30T00:00:00Z</updated>
    <id>https://chameth.com/</id>
    <author>
        <name>Chris Smith</name>
    </author>
    <entry>
        <title>Migrating from GitHub to Forgejo</title>
        <link href="https://chameth.com/migrating-from-github-to-forgejo/"/>
        <updated>2026-04-30T00:00:00Z</updated>
        <id>https://chameth.com/migrating-from-github-to-forgejo/</id>
        <content xml:lang="en" type="html">&lt;p&gt;When Microsoft bought GitHub in 2018 my kneejerk reaction — like so many others — was to start looking for alternatives. For a while I self hosted a &lt;a href=&#34;https://about.gitea.com/&#34;&gt;Gitea&lt;/a&gt; instance but I never totally bought into it: some repositories I still pushed to GitHub, some I pushed to Gitea and they got mirrored, and I ended up causing myself problems when I got the two confused. Part of the problem was that the GitHub UI was faster and cleaner than Gitea’s at the time; using Gitea felt like a chore compared to GitHub. I ended up not maintaining it and eventually binning it and just going back to GitHub.&lt;/p&gt;
&lt;figure class=&#34;image right&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/migrating-from-github-to-forgejo/unicorn.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/migrating-from-github-to-forgejo/unicorn.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/migrating-from-github-to-forgejo/unicorn.png&#34; alt=&#34;A screenshot of the GitHub error page, featuring an angry-looking Unicorn.&#34; loading=&#34;lazy&#34; width=&#34;575&#34; height=&#34;477&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;An all-too familiar unicorn&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Fast forward eight years, and GitHub is about what we all imagined when Microsoft bought it. If you take the most pessimistic way of counting, they have &lt;a href=&#34;https://mrshu.github.io/github-statuses/&#34;&gt;zero nines of reliability&lt;/a&gt;&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:1&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;. If you take the most generous, they have a single nine. That’s around 30 minutes of downtime every day. It feels like it must be more than that, given how many times you see the damned unicorn.&lt;/p&gt;
&lt;p&gt;Uptime aside, they’ve crowbarred LLMs in all over the place; they &lt;a href=&#34;https://github.com/actions/create-release/issues/119&#34;&gt;don’t have the time&lt;/a&gt; to maintain official GitHub actions; the ones they do maintain have &lt;a href=&#34;https://github.com/actions/toolkit/compare/09cb71a033743b7545c8a9181facc06d7d6012ba...7ae5c2f423367fd11aa625ddcc0bbb0a8e5de5fa&#34;&gt;Copilot running roughshod all over them&lt;/a&gt;&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:2&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;; actions themselves are slow to run&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:3&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt;, often have weird transient failures, and there are &lt;em&gt;so many&lt;/em&gt; footguns that I’m pretty sure GitHub actions should be an entire category in the CWE top 10.&lt;/p&gt;
&lt;p&gt;Speaking of security, while I was drafting this post a remote code execution &lt;a href=&#34;https://www.wiz.io/blog/github-rce-vulnerability-cve-2026-3854&#34;&gt;was reported&lt;/a&gt; and the details are… impressive. The actual security issue was a pretty stupid oversight. Those happen. But they end up being able to execute code as a globally shared &lt;code&gt;git&lt;/code&gt; user with access to all the other repositories on the node. What? I can’t quite get my head around it. No sandboxing, no containers, it just runs as a &lt;code&gt;git&lt;/code&gt; user?&lt;/p&gt;
&lt;p&gt;So, yeah, I’m not a fan of GitHub in its current state. A few months back I took the plunge and set up &lt;a href=&#34;https://forgejo.org/&#34;&gt;Forgejo&lt;/a&gt;, a fork of Gitea where they did radical things like add tests and ensure that it’s community operated not twisted for commercial use. Forgejo also potentially &lt;a href=&#34;https://dustri.org/b/carrot-disclosure-forgejo.html&#34;&gt;has some fun security problems&lt;/a&gt;&lt;sup id=&#34;fnref:4&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:4&#34; role=&#34;doc-noteref&#34;&gt;4&lt;/a&gt;&lt;/sup&gt;, but the way I host it mitigates more-or-less all problems. It’s also slightly less shocking when an open source project has lapses in basic security versus a $2 trillion corporation.&lt;/p&gt;
&lt;p&gt;It’s taken a bit of doing, but I’m very happy with the setup I have.&lt;/p&gt;
&lt;h3 id=&#34;the-basic-setup&#34;&gt;The basic setup&lt;/h3&gt;
&lt;p&gt;I run all my server software using Docker, and Forgejo is no exception. They have &lt;a href=&#34;https://forgejo.org/docs/latest/admin/installation/docker/&#34;&gt;decent documentation&lt;/a&gt; on how to get it running, and publish rootless images which is nice (and diminishes my urge to make an &lt;a href=&#34;https://chameth.com/artisanal-docker-images/&#34;&gt;artisanal version myself&lt;/a&gt;). You can set configuration options using environment variables, which is perfect for containers, although the names can end up a bit unwieldy. Like this thing: &lt;code&gt;FORGEJO__repository.signing__SIGNING_NAME=Chris Smith&lt;/code&gt;. It gets the job done, but the environment-to-ini mapping is pretty ugly.&lt;/p&gt;
&lt;p&gt;I didn’t want to expose Forgejo publicly, as I wanted to avoid having to try and secure it&lt;sup id=&#34;fnref:5&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:5&#34; role=&#34;doc-noteref&#34;&gt;5&lt;/a&gt;&lt;/sup&gt;, or dealing with bots scraping it, or users signing up, and so on. I wanted somewhere to store my git repositories, handle CI for me, and mirror them somewhere public for other people to see and interact with. If you’ve read some of my other posts you’ll probably guess where this is heading: &lt;a href=&#34;https://tailscale.com/&#34;&gt;Tailscale&lt;/a&gt;. Alongside the Forgejo container, I run a Tailscale instance with a &lt;code&gt;serve.json&lt;/code&gt; that covers both the web frontend and the SSH listener used for git operations:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;TCP&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;22&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;            &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;TCPForward&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;forgejo:2222&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;443&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;            &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;HTTPS&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-kc&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;Web&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;${TS_CERT_DOMAIN}:443&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;            &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;Handlers&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;                &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;                    &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;Proxy&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;http://forgejo:3000&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;                &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;            &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;AllowFunnel&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;${TS_CERT_DOMAIN}:443&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-kc&#34;&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This allows me to interact normally with Forgejo from any of my devices or servers (which all run Tailscale), while not exposing it to the Internet at all. It also handles the TLS certificates automatically, which saves me setting up something else to do that.&lt;/p&gt;
&lt;p&gt;For CI, I run the Forgejo runner image on the same server, and gave it a &lt;code&gt;dind&lt;/code&gt; container to use to actually run the workloads. &lt;code&gt;dind&lt;/code&gt; is the cutesy name for Docker-in-Docker, basically a Docker daemon running inside a Docker container. That keeps the CI workload separated nicely from the “production” workload running on the server, which is nice from both a security and a monitoring point of view.&lt;/p&gt;
&lt;h3 id=&#34;the-uncanny-valley-of-actions&#34;&gt;The uncanny valley of actions&lt;/h3&gt;
&lt;p&gt;Forgejo actions are basically like GitHub actions: they run a series of steps using one or more container images. You define those steps in the exact same clunky YAML format as with GitHub. The big difference between them is that GitHub defaults to using an absolutely massive base image filled with &lt;a href=&#34;https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Readme.md&#34;&gt;out-of-date preinstalled software&lt;/a&gt;, and Forgejo leaves that for the administrator to configure. You could, of course, just use the same image as GitHub, but lugging around a multi-gigabyte image that doesn’t even have recent versions of the software I want isn’t really my style. Instead, I set about using a plain &lt;code&gt;debian&lt;/code&gt; image as the base, and then did a face-palm when I tried to run a “normal” action and remembered they’re all written in JavaScript, so expect node to exist.&lt;/p&gt;
&lt;p&gt;Accommodating JavaScript isn’t really my style, either, so I did the very sensible thing of writing &lt;a href=&#34;https://github.com/csmith/actions&#34;&gt;my own suite of actions in Go&lt;/a&gt;. Being written in Go means they can be compiled statically, published as a container, and manage their own very limited dependencies. For example, the &lt;code&gt;checkout&lt;/code&gt; action is built into an &lt;code&gt;alpine&lt;/code&gt; container with &lt;code&gt;git&lt;/code&gt; added, while the &lt;code&gt;dockerbuild&lt;/code&gt; action uses the &lt;code&gt;buildah&lt;/code&gt; image as a base. Each image has the tools it needs, pinned to a recent version, and nothing more. This is very much not a sensible approach to take for most people, but it made me happy. With six basic actions, I could replace almost all the ad-hoc workflows I had in GitHub&lt;sup id=&#34;fnref:6&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:6&#34; role=&#34;doc-noteref&#34;&gt;6&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Other than the base images and my self-inflicted JavaScript machinations, everything works the same as GitHub. But faster. So much faster. Most of my CI workflows finish on Forgejo before GitHub would even have allocated a runner to the job&lt;sup id=&#34;fnref:7&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:7&#34; role=&#34;doc-noteref&#34;&gt;7&lt;/a&gt;&lt;/sup&gt;. And I can actually view the logs without them glitching out.&lt;/p&gt;
&lt;h3 id=&#34;step-aside-dependabot-here-comes-renovate&#34;&gt;Step aside Dependabot, here comes Renovate&lt;/h3&gt;
&lt;p&gt;One of the boons and/or banes of hosting code on GitHub is access to Dependabot&lt;sup id=&#34;fnref:8&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:8&#34; role=&#34;doc-noteref&#34;&gt;8&lt;/a&gt;&lt;/sup&gt;. It’s a tool that monitors your dependencies, and automatically submits PRs when new versions are available. It also does security alerts, but they’re comically bad for Go at least&lt;sup id=&#34;fnref:9&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:9&#34; role=&#34;doc-noteref&#34;&gt;9&lt;/a&gt;&lt;/sup&gt;. I do like having dependencies automatically handled, especially with &lt;a href=&#34;https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns&#34;&gt;a cooldown&lt;/a&gt;, which is how I had Dependabot configured before moving.&lt;/p&gt;
&lt;p&gt;To replace it, I’ve configured &lt;a href=&#34;https://github.com/renovatebot/renovate&#34;&gt;Renovate&lt;/a&gt;, an open source alternative. I’d seen it used a bunch before: both by projects on GitHub who prefer it over Dependabot, and by people using GitLab and other platforms where Dependabot isn’t. It mostly does the same things, but Renovate has a few very nice extras.&lt;/p&gt;
&lt;p&gt;Firstly, it supports transcluding config from another repo. So in each of my many, many projects, I just have this stub of a renovate config:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;$schema&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;https://docs.renovatebot.com/renovate-schema.json&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;extends&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;local&amp;gt;meta/renovate&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  &lt;span class=&#34;chroma-p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then the actual config lives in a &lt;code&gt;default.json&lt;/code&gt; in the &lt;code&gt;meta/renovate&lt;/code&gt; repository. I &lt;a href=&#34;https://github.com/csmith/forgejo-renovate&#34;&gt;mirror that repository&lt;/a&gt; to GitHub, if you want to have a look. This is an amazing feature. When I enabled cooldowns in my Dependabot config it was a &lt;em&gt;slog&lt;/em&gt; to go through all my active repositories and make the same change over and over again. With Renovate I can configure that centrally. As far as I know Dependabot can’t do anything like that, even for enterprises.&lt;/p&gt;
&lt;p&gt;Within that central config I use some of the other nice features. It has the ability to auto merge changes. I use this to automatically accept changes from trusted projects, or my own libraries:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;matchManagers&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;gomod&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;matchPackagePatterns&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;^github\\.com\\/csmith\\/&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;^chameth\\.com\\/&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;matchUpdateTypes&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;minor&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;patch&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;automerge&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;minimumReleaseAge&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;0 days&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;matchManagers&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;gomod&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;matchPackagePatterns&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;^github\\.com\\/csmith\\/&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;^chameth\\.com\\/&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;matchUpdateTypes&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;major&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;minimumReleaseAge&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;0 days&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This takes any minor or patch release of my libraries, and automerges it with no cooldown. For major version changes it overrides the cooldown but doesn’t automerge. To automerge Dependabot PRs you have to write an action, implement that logic yourself, and hope you’ve successfully avoided all the footguns inherent in that.&lt;/p&gt;
&lt;p&gt;Speaking of major version updates, Renovate also has an option to automatically update the imports in Go packages when it’s offering a major update. In the config I have:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;postUpdateOptions&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;gomodTidy&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;gomodUpdateImportPaths&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Which means that every time it does an update it’ll run &lt;code&gt;go mod tidy&lt;/code&gt;, and it’ll update the import paths. In Go, &lt;code&gt;v2&lt;/code&gt; of a module has a different import path to &lt;code&gt;v1&lt;/code&gt; (so they can coexist if transient dependencies need different versions), so usually you have to go through and find and replace those paths. Renovate handles that for me now! I still have to address whatever breaking changes are in the new version, but there’s a lot less grunt work.&lt;/p&gt;
&lt;p&gt;It can also run custom commands&lt;sup id=&#34;fnref:10&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:10&#34; role=&#34;doc-noteref&#34;&gt;10&lt;/a&gt;&lt;/sup&gt;, so for example in my &lt;a href=&#34;https://github.com/csmith/legotapas&#34;&gt;legotapas&lt;/a&gt; project I have this extra snippet in the &lt;code&gt;renovate.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;postUpgradeTasks&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;commands&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;sh -c &amp;#39;rm plate_*.go&amp;#39;&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;go run ./cmd/generate&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;executionMode&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;branch&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;fileFilters&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;*.go&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I won’t go into the gory details, but this handles automatically regenerating a bunch of files that change every time a certain dependency is updated. Previously I had a separate action to do that on GitHub; just being able to drop commands in the config file is much nicer.&lt;/p&gt;
&lt;p&gt;To actually run Renovate, I simply have a workflow in Forgejo on a schedule that uses their official Docker image. It also triggers if I update the config, which is nice when I add a new rule, as it’ll immediately update the open PRs to comply. Overall, Renovate has felt like a pretty big quality-of-life upgrade.&lt;/p&gt;
&lt;h3 id=&#34;odd-problems&#34;&gt;Odd problems&lt;/h3&gt;
&lt;p&gt;Not everything has been smooth sailing. Sometimes when Renovate automerged a change, it would just… disappear. The PR was closed as merged, but the commit never landed on master. It wasn’t a huge issue as the next Renovate run would just recreate it, but it would then &lt;em&gt;disable&lt;/em&gt; the automerge and I’d get confused as to why. It turns out there was a major problem with how I had everything set up. Renovate was telling Forgejo to automerge when the required checks pass, but I had no branch protection rules set up in Forgejo, so as soon as the PR was created it was eligible to merge. As the CI was so fast I hadn’t noticed that the automerges were happening &lt;em&gt;before&lt;/em&gt; the CI results were posted.&lt;/p&gt;
&lt;p&gt;The disappearing commits were because the repository was getting into a bad state when Renovate tried to automerge multiple changes instantly in the same repo. Forgejo has a bunch of maintenance commands that helped to fix that problem, and I then wrote a little script to enable branch protection rules so the CI would actually run. The automerging has been smooth since.&lt;/p&gt;
&lt;p&gt;Another issue I had early on was that merging would fail because the GPG keyring was locked. I had Forgejo configured to sign all the automatic commits, so you can verify what commits came from my infrastructure. Forgejo is the only process using the keyring, so I wasn’t sure why it was becoming locked. It turns out that GnuPG uses the hostname as part of the lockfile, and because Forgejo was running in Docker it was getting random hostnames based on the container ID. Explicitly setting a hostname made GnuPG and by extension Forgejo much happier.&lt;/p&gt;
&lt;h3 id=&#34;webhooks-and-updating&#34;&gt;Webhooks and updating&lt;/h3&gt;
&lt;p&gt;Forgejo has this amazing feature: you can add a webhook that’s called for all your repos. GitHub, for some reason, supports this for organisations but not people. Even though the two are treated the same in a lot of places. It doesn’t seem like it would be hard, and it’s such an obvious thing to want, but… no. I wrote a &lt;a href=&#34;https://github.com/csmith/webhooked&#34;&gt;whole tool&lt;/a&gt; to add webhooks for me to get around this. Now I don’t need it, Forgejo does the right thing!&lt;/p&gt;
&lt;p&gt;One particularly interesting use of these webhooks has been making my Docker containers auto-update when a new image is pushed. I previously had a webhook receiver for a few projects that would update them automatically, but GitHub package notifications were extremely laggy. Every other webhook fired within a minute or so of the event happening, but package webhooks would sometimes take 30-60+ minutes to fire. I opened a support case with GitHub and it got escalated to engineering and fixed about 10 weeks later&lt;sup id=&#34;fnref:11&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:11&#34; role=&#34;doc-noteref&#34;&gt;11&lt;/a&gt;&lt;/sup&gt;. It was better for a while, and then got worse again. Trying to do continuous deployment with a 30 minute random lag was frustrating.&lt;/p&gt;
&lt;p&gt;So now I had reliable webhooks, but the update process was a bit gross. I have a hangup about internet-facing services not having access to the Docker socket. It just feels like too big a security risk. So I had a process that received hooks, and then another that figured out if containers needed to be updated and restarted them. Adding a new container to the system meant recompiling the second process, which was a massive faff.&lt;/p&gt;
&lt;p&gt;Fortunately, I was saved from this mess by my friend &lt;a href=&#34;https://greboid.com/&#34;&gt;Greg&lt;/a&gt;. He wrote &lt;a href=&#34;https://github.com/greboid/adze&#34;&gt;adze&lt;/a&gt;, a tool that receives webhooks and uses Docker Compose to update matching containers. He wrote &lt;a href=&#34;https://greboid.com/ramblings/2026/04/Keeping-containers-up-to-date&#34;&gt;a blog post about it&lt;/a&gt;. It’s all one process, but that’s no longer a blocker for me because the webhooks can be delivered privately from my Forgejo instance without exposing adze to the internet. It finds matching containers automatically based on the images they’re using&lt;sup id=&#34;fnref:12&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:12&#34; role=&#34;doc-noteref&#34;&gt;12&lt;/a&gt;&lt;/sup&gt;, and combined with Forgejo not forcing me to add a webhook to every single repository makes my life so much easier. I write some code, push it to Forgejo, and within a minute or so it’s running on my dev instance.&lt;/p&gt;
&lt;p&gt;I have IRC notifications set up for when adze does updates, and I still have some old ones that announce GitHub package webhooks. I keep seeing things like this and chuckling:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;15:44:42 &amp;lt;@ircjag&amp;gt; [ADZE] chameth.com: pending updating git.yak-wall.ts.net/public/chameth.com
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;15:44:45 &amp;lt;@ircjag&amp;gt; [ADZE] chameth.com: success updating git.yak-wall.ts.net/public/chameth.com
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;17:11:54 &amp;lt;@ircjag&amp;gt; [GHCR] Container pushed to csmith/chameth.com:dev.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I pushed a change at 15:43, Forgejo built it and pushed the image (to both its own registry and to GitHub), then at 15:44 adze updated the container to the new version. An hour and a half later GitHub gets around to delivering its own webhook.&lt;/p&gt;
&lt;h3 id=&#34;mirroring-and-prs&#34;&gt;Mirroring and PRs&lt;/h3&gt;
&lt;p&gt;The final piece I had to figure out was how to actually interact with people. Most of my projects are small and don’t get a massive amount of public contributions, but the option should still be there. And ideally not involve anyone having to figure out how to e-mail patches&lt;sup id=&#34;fnref:13&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:13&#34; role=&#34;doc-noteref&#34;&gt;13&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Mirroring outwards is simple: Forgejo supports it natively. Whenever I push to Forgejo, it pushes out to the corresponding repo on GitHub. I’ve also started mirroring some projects to &lt;a href=&#34;https://codeberg.org/&#34;&gt;Codeberg&lt;/a&gt;, to avoid having GitHub as my single “public” presence. But what happens when I get a pull request? If I merge it in the GitHub web UI, Forgejo will push its own branch over the top and get rid of it&lt;sup id=&#34;fnref:14&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:14&#34; role=&#34;doc-noteref&#34;&gt;14&lt;/a&gt;&lt;/sup&gt;. I could pull the changes, merge them, and push the result to Forgejo but then I don’t get the benefit of any CI.&lt;/p&gt;
&lt;p&gt;I ended up writing a small private tool that deals with this for me. I give it a GitHub PR, and it effectively imports it into Forgejo for me. I created a &lt;code&gt;prs&lt;/code&gt; organisation, and it’ll create a fork of the project in that org if it doesn’t exist. Then it pushes the PR contents to a branch, and creates a PR from the &lt;code&gt;prs&lt;/code&gt; repo into the main one. It runs as a user which can write to the PRs org, but only submit pull requests to public repositories outside of it.&lt;/p&gt;
&lt;p&gt;When the PR is imported, I can review it in the Forgejo UI, and then I can approve the CI workflows for that particular PR. This gives me some extra defence-in-depth in case anyone is trying to abuse one of those action footguns. Once the CI passes, I can merge it as normal in the Forgejo UI, it gets mirrored out to GitHub, and GitHub marks the PR as merged automatically. It’s not the most elegant solution, but the actual workflow is pretty smooth, especially for the low volumes of PRs I get.&lt;/p&gt;
&lt;p&gt;With that, I have Forgejo set up to do everything I need from a git host, and I’m no longer hamstrung every time GitHub goes down.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr/&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;This way of counting isn’t &lt;a href=&#34;https://evanhahn.com/in-defense-of-githubs-poor-uptime/&#34;&gt;particularly fair&lt;/a&gt;, but it’s somewhat realistic. GitHub break out their uptime for things like git operations, pull requests, actions, webhooks and packages. Realistically if any of those independent things are down then my overall workflow doesn’t, well, work. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:1&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;
&lt;p&gt;To be clear, I’m not objecting to the use of Copilot here. It’s a tool. But if you use a tool to do something fairly simple and it spews out 35 commits, including at least 5 reverts, then either it’s a terrible tool or you’re using it very wrong. I didn’t hunt down this example, I came across it while trying to debug something and wasted a bunch of time trying to figure out what on earth was going on. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:2&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:3&#34;&gt;
&lt;p&gt;You could self-host runners, which may help, except they’ve already threatened to &lt;a href=&#34;https://github.blog/changelog/2025-12-16-coming-soon-simpler-pricing-and-a-better-experience-for-github-actions/&#34;&gt;charge you for the privilege of bringing your own runners&lt;/a&gt;. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:3&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:4&#34;&gt;
&lt;p&gt;If people could stop finding RCEs in git services for a few days so I can stop redrafting this intro, that’d be great. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:4&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:5&#34;&gt;
&lt;p&gt;I’m feeling pretty smug about this decision after reading about the security issues. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:5&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:6&#34;&gt;
&lt;p&gt;There’s one holdout: my &lt;a href=&#34;https://github.com/csmith/dockerfiles/&#34;&gt;dockerfiles repo&lt;/a&gt; which is a very special snowflake with a very complex workflow and I’m still deciding how to handle it. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:6&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:7&#34;&gt;
&lt;p&gt;Which is to be expected, given I’ve got a server dedicated to one person, and they’re trying to serve however-many million users at once. That doesn’t make it any less nice, though! &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:7&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:8&#34;&gt;
&lt;p&gt;I’d link to it but there’s nowhere really good to link to. It’s just been subsumed into the ‘security’ functions of GitHub, along with a bunch of copilot nonsense. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:8&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:9&#34;&gt;
&lt;p&gt;The go project maintain a &lt;a href=&#34;https://pkg.go.dev/vuln/&#34;&gt;vulnerability database&lt;/a&gt; that includes details about which exact symbols are affected, and the &lt;a href=&#34;https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck&#34;&gt;govulncheck&lt;/a&gt; tool uses this to only report security issues in code that’s actually used. Dependabot doesn’t do that: it constantly alerts about code that’s not imported, so won’t ever be shipped. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:9&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:10&#34;&gt;
&lt;p&gt;In the default config it doesn’t allow anything of the sort, of course. You can whitelist individual commands, but because this is a private server I can merrily whitelist &lt;code&gt;.*&lt;/code&gt; and do whatever I like. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:10&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:11&#34;&gt;
&lt;p&gt;The turnaround time isn’t particularly impressive, but the fact that they kept the support case open, and the (human!) support agent updated me several times is. I don’t have a lot of good things to say about GitHub, but I don’t think I’ve ever had a support experience that good from a company of a similar size. Especially as I wasn’t a paying customer at the time. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:11&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:12&#34;&gt;
&lt;p&gt;Why didn’t I do that in the first place? It seems so obvious now… &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:12&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:13&#34;&gt;
&lt;p&gt;I really like the &lt;em&gt;idea&lt;/em&gt; of an e-mail based workflow, but I absolutely hate actually doing it. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:13&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:14&#34;&gt;
&lt;p&gt;This is what happened to me a lot when I was half using Gitea. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:14&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</content>
    </entry>
    <entry>
        <title>Home Automation Without the Megacorps</title>
        <link href="https://chameth.com/home-automation-without-megacorps/"/>
        <updated>2025-05-21T00:00:00Z</updated>
        <id>https://chameth.com/home-automation-without-megacorps/</id>
        <content xml:lang="en" type="html">&lt;p&gt;I first experimented with home automation in 2016, by picking up a Samsung
“SmartThings” hub. It was terrible. The UI to configure things was slow and
clunky, firmware updates were applied whether you wanted them or not, and
everything stopped working if their cloud services stopped. You were also locked
into whatever integrations they deigned to support, of course. After that broke
for the umpteenth time I scaled back and for years the closest I got to home
automation was a couple of Hue bulbs.&lt;/p&gt;
&lt;p&gt;Recently I’ve been building it out again, though. This time using off-the-shelf
components that interop using Zigbee, open-source software, and some code I
wrote myself. It’s great; it runs entirely locally and has had basically zero
downtime. The Zigbee ecosystem lets me integrate all sorts of things without
having to spend lots of money on “smart” alternatives. I think I’ve spent less
on this incarnation than I did on the original SmartThings hub all those years
ago (even without adjusting for inflation!).&lt;/p&gt;
&lt;h3 id=&#34;my-current-setup&#34;&gt;My current setup&lt;/h3&gt;
&lt;p&gt;I run everything on a Raspberry Pi 4, with a Sonoff USB Zigbee adapter based
on the CC2652P chipset&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:1&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;. Interfacing with the Zigbee stack is handled by
&lt;a href=&#34;https://www.zigbee2mqtt.io/&#34;&gt;zigbee2mqtt&lt;/a&gt; (z2m for short), an open-source project that
basically bridges your devices to an MQTT broker&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:2&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;. When a device reports some
data, it will send a new message over MQTT; when you want to make a device do
something you just post a message back. It’s incredibly lightweight, but
supports a huge array of devices out of the box. And as it’s just using MQTT,
it’s trivial to integrate with other software or build on top of.&lt;/p&gt;
&lt;!--more--&gt;
&lt;p&gt;I know a lot of people building out this kind of home automation use
&lt;a href=&#34;https://www.home-assistant.io/&#34;&gt;Home Assistant&lt;/a&gt;, but I don’t get on with it
terribly well. It felt incredibly sluggish, and the entire project is just a
bit… much. I want a binary or package or docker image I can just run, not an
entire operating system. That’s not how computers are meant to work! In contrast,
z2m is simple to set up, super light weight and responsive.&lt;/p&gt;
&lt;p&gt;Anyway. z2m exposes Zigbee devices over MQTT, so I wrote some code in Go to
connect to the MQTT broker, and listen to the messages. It’s grown a bit beyond
this now, and I’m skipping some boring bits like error handling and JSON
parsing, but at first I had something like:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-k&#34;&gt;for&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;message&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;topic&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;c&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;chroma-nf&#34;&gt;ReadSlices&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-w&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;chroma-k&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-nb&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;topic&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-o&#34;&gt;==&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-s&#34;&gt;&amp;#34;zigbee2mqtt/desk-button&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;chroma-k&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;strings&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;chroma-nf&#34;&gt;Contains&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;message&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-s&#34;&gt;&amp;#34;single&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;chroma-nx&#34;&gt;c&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;chroma-nf&#34;&gt;Publish&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;chroma-nb&#34;&gt;byte&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;chroma-s&#34;&gt;&amp;#34;{\&amp;#34;state\&amp;#34;: \&amp;#34;TOGGLE\&amp;#34;}&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;),&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;chroma-s&#34;&gt;&amp;#34;zigbee2mqtt/room-lights/set&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;chroma-w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I think I actually made this harder to read by trying to simplify it here, but
hopefully you can follow that it was relatively straight forward to listen
for a particular action to happen (in this case me “single”-pressing on the
device called “desk-button”) and then make another device do something in
response (toggling the state of the “room-lights” device).&lt;/p&gt;
&lt;p&gt;So what actually are these devices? At present in z2m I have the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Some generic buttons&lt;/li&gt;
&lt;li&gt;An air quality sensor (USB powered), and a separate temperature sensor (battery powered)&lt;/li&gt;
&lt;li&gt;A light switch&lt;/li&gt;
&lt;li&gt;Some 240V relay modules, that I use to turn non-smart devices on and off&lt;/li&gt;
&lt;li&gt;Some USB relay modules, for the same purpose&lt;/li&gt;
&lt;li&gt;Some “smart” plugs that I mostly use to monitor power usage&lt;/li&gt;
&lt;li&gt;A motorised blind roller&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I think all of these are either Tuya or Moes branded, and were all sourced from
AliExpress. You can get the same things from Amazon or elsewhere, but they tend
to be 3-4x more expensive for the same product.&lt;/p&gt;
&lt;p&gt;One of the cool things about Zigbee devices is that the powered ones work
together to create a mesh network, so you don’t have to worry about network
repeaters or signal strength like you do with Wi-Fi networks. z2m even makes
a map showing the connections:&lt;/p&gt;
&lt;figure class=&#34;image full&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/home-automation-without-megacorps/zigbee-map.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/home-automation-without-megacorps/zigbee-map.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/home-automation-without-megacorps/zigbee-map.png&#34; alt=&#34;A &amp;#34;map&amp;#34; showing how Zigbee devices connect to one another&#34; loading=&#34;lazy&#34; width=&#34;1093&#34; height=&#34;651&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;The network map produced by z2m. You can see how all the powered devices form a mesh that the lower power ones can connect to.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3 id=&#34;some-interesting-automations&#34;&gt;Some interesting automations&lt;/h3&gt;
&lt;figure class=&#34;image right&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/home-automation-without-megacorps/relay.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/home-automation-without-megacorps/relay.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/home-automation-without-megacorps/relay.jpg&#34; alt=&#34;A Zigbee relay and some wago connectors wired up in a project box&#34; loading=&#34;lazy&#34; width=&#34;325&#34; height=&#34;500&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;A Zigbee relay spliced into the power cable for the fan&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;So what do I actually do with all of this? Some of it is just what I’ll call
“laziness automation”: I want to be able to turn on the lights without getting
up and walking to the light switch. So there’s a Zigbee button on my desk that
does it. Then there are some less manual automations: my blinds are
automatically closed at sunset&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:3&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt;, for example. It feels a bit trivial, but it’s
surprisingly nice just not to have to think about that at all.&lt;/p&gt;
&lt;p&gt;Another nice quality of life automation is the light I have above my monitor.
It’s USB powered, and I now have it going through a Zigbee USB relay that lets
it be toggled on and off. Then I have a small agent running on my desktop that
turns the light on when the computer is unlocked, and off when it locks or
shuts down. I keep meaning to make a “film mode” that detects when I’m watching
a film, and turning all the lights and blinds down, but I haven’t got around
to it yet.&lt;/p&gt;
&lt;p&gt;The most complex automation is probably for a window fan. It’s not smart in
any way, so I cut the power cable and inserted a Zigbee relay. The whole thing
is housed in a little project box to keep it secure. The relay basically acts
as a switch: the live wire leading to the fan runs to the “normally open”&lt;sup id=&#34;fnref:4&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:4&#34; role=&#34;doc-noteref&#34;&gt;4&lt;/a&gt;&lt;/sup&gt;
contact, while the upstream power goes to “common”. When the relay is told to
turn the fan on, the two get connected and the fan gets power. Oh, it’s also
not actually a window fan, but I designed and 3D printed some adapters so that
it sits right in the window.&lt;/p&gt;
&lt;p&gt;So now we have a window fan that can be turned off and on automatically. But I don’t
want to have to press a button to do that. I’m lazy, remember? Instead, I made
it so that the Go code constantly monitors the temperature reported by the air
quality sensor, and queries the free &lt;a href=&#34;https://openweathermap.org/&#34;&gt;OpenWeatherMap API&lt;/a&gt;
to get the rough temperature outside. It can then turn the fan on if the room is
too hot, and outside is cool enough to make a difference (there’s no point in
blowing hotter air in!).&lt;/p&gt;
&lt;figure class=&#34;image left&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/home-automation-without-megacorps/fan-graph.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/home-automation-without-megacorps/fan-graph.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/home-automation-without-megacorps/fan-graph.png&#34; alt=&#34;A graph of room temperature, slowly rising to 23.5 degrees, then sharply dropping to 19, rising to 20, dropping to 19 again, etc&#34; loading=&#34;lazy&#34; width=&#34;540&#34; height=&#34;500&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;A graph of room temperature, showing the effect of the fan being turned on and off&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I don’t want it to do that all the time though. The room doesn’t need to be
cooled if I’m not around. There are various Zigbee presence sensors you can get,
but the cheaper ones seem to be of dubious quality. Instead, I wrote some code
to guess whether I’m present. It uses the state of the monitor light
as a proxy for “is the computer in use”, and then makes some guesses based on
the last time the computer was used and the current time (if I turn the computer
off at midnight it probably means I’m going to sleep; if I turn it off at 2pm
it probably means I’m going out somewhere).&lt;/p&gt;
&lt;p&gt;You can see in the graph that the fan has a decent impact on temperature. I
coded it to cool the room to 19 degrees, but then not turn on again until it
was above 20. This prevents it flip-flopping on and off constantly. You can
clearly see the pattern in the graph, as it actively cools and then slowly
the room warms back up. This arrangement is much better than being woken up at
5am because it’s painfully cold. Trust me.&lt;/p&gt;
&lt;h3 id=&#34;bonuses-metrics-and-3d-printer&#34;&gt;Bonuses: metrics and 3D printer&lt;/h3&gt;
&lt;p&gt;One of the things my custom Go code does is collate all the various stats
reported by the Zigbee devices, and inserts them into a &lt;a href=&#34;https://victoriametrics.com/&#34;&gt;VictoriaMetrics&lt;/a&gt;
database. I originally hosted this on the Pi itself, and it performed fine, but
I’ve since moved it onto a server so that I can use it for some other things
as well.&lt;/p&gt;
&lt;p&gt;I set up Grafana to point to VM, and can create dashboards showing power usage,
what devices are turned on when, and a bunch of environmental conditions. This
also makes it easy to spot how good the data coming from the devices are. For
example, the air quality sensor reports a figure for the amount of Carbon Dioxide
in the air, as well as the amount of Volatile Organic Compounds (VOCs). The
graphs are basically identical, but on a different scale. It turns out this
particular device has no actual way of detecting CO₂, so it just “calculates”
it from the VOCs figure. Useful to know if you want to actually use that data.&lt;/p&gt;
&lt;p&gt;Recently I came across &lt;a href=&#34;https://github.com/torbenconto/bambulabs_api&#34;&gt;a go library for interacting with Bambu Lab printers&lt;/a&gt;,
so I’ve also hooked that into my automation. It exports metrics about the
printer, so I now have a way of seeing what’s going on when I’m not physically
present&lt;sup id=&#34;fnref:5&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:5&#34; role=&#34;doc-noteref&#34;&gt;5&lt;/a&gt;&lt;/sup&gt;. I even added a HTTP endpoint (exposed over &lt;a href=&#34;https://tailscale.com/&#34;&gt;Tailscale&lt;/a&gt;) that
shows pictures from the built-in camera.&lt;/p&gt;
&lt;p&gt;On the automation front, I’ve made it automatically turn off the printer’s light
when it finishes a print, and also added push notifications via &lt;a href=&#34;https://pushover.net/&#34;&gt;PushOver&lt;/a&gt;
whenever the state changes. No longer will I be sat in another room blissfully
unaware it ran out of filament seconds after I walked out the door!&lt;/p&gt;
&lt;h3 id=&#34;was-it-worth-it&#34;&gt;Was it worth it?&lt;/h3&gt;
&lt;p&gt;These days you could probably just buy some kind of ‘smart home’ hub that works
well enough, and do most of what I’ve done with a lot less effort and no coding.
I’m still of the opinion that for something so essentially &lt;em&gt;local&lt;/em&gt;, it should
itself be managed entirely locally. I don’t trust companies like Google or
Amazon not to kill their products, or change or remove an API I rely on.&lt;/p&gt;
&lt;p&gt;Obviously the “Not Invented Here” approach of coding everything yourself doesn’t
suit everyone, but as someone who enjoys coding and enjoys having things work
&lt;em&gt;just so&lt;/em&gt; it works very well for me.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr/&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;It turns out the chipset is important. I initially used a similar Sonoff
dongle that used a CC2531 chipset, and its performance wasn’t great. It often
failed to pair with new devices, and dropped links to existing ones. The
&lt;a href=&#34;https://www.zigbee2mqtt.io/advanced/zigbee/02_improve_network_range_and_stability.html&#34;&gt;zigbee2mqtt docs&lt;/a&gt;
do explicitly advise against the CC2531 chips for that reason. At the time they
recommended CC2652Ps, so that’s what I went with. If you’re starting new I’d
go with whatever their latest recommendation was. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:1&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;
&lt;p&gt;MQTT is basically a network protocol for publishing and subscribing to
arbitrary messages. A broker is the thing that sits in the middle and routes
the messages. I use &lt;a href=&#34;https://mosquitto.org/&#34;&gt;Mosquitto&lt;/a&gt; but any will do. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:2&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:3&#34;&gt;
&lt;p&gt;They originally also re-opened at sunrise but that was a terrible
mistake. Who knew the sun rose so early?! &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:3&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:4&#34;&gt;
&lt;p&gt;“open” meaning “there’s a gap so it doesn’t work” not
“open for business”. This confusion in terminology also extends to drawbridges. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:4&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:5&#34;&gt;
&lt;p&gt;I use the printer in LAN mode, which means there’s no way to monitor it
from a phone, even if you’re connected to the same network still. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:5&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</content>
    </entry>
    <entry>
        <title>Further adventures in 3D printing</title>
        <link href="https://chameth.com/further-adventures-in-3d-printing/"/>
        <updated>2024-08-04T00:00:00Z</updated>
        <id>https://chameth.com/further-adventures-in-3d-printing/</id>
        <content xml:lang="en" type="html">&lt;figure class=&#34;image right&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/further-adventures-in-3d-printing/p1s.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/further-adventures-in-3d-printing/p1s.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/further-adventures-in-3d-printing/p1s.png&#34; alt=&#34;Marketing image of the Bambu Labs P1S&#34; loading=&#34;lazy&#34; width=&#34;404&#34; height=&#34;400&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;The Bambu Labs P1S.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Not quite a year ago, I bought a Sovol SV06 3D printer and &lt;a href=&#34;https://chameth.com/adventures-in-3d-printing/&#34;&gt;wrote about&lt;/a&gt;
my initial experiences. At the end I joked:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I’m sure there will be more problems, but hopefully I get at least a week or
so before anything else goes wrong. Maybe I’ll print an “X days since the
last SV06 problem” counter…&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That turned out to be more prophetic than I expected. It became a running joke
that I’d flip the imaginary “days since” sign back to 0 almost every time I
printed. I still really enjoyed having a printer, though: it’s so useful to
be able to be able to go from an idea to a CAD drawing to a physical object
in the space of minutes or hours. So when Bambu Labs reduced the price of the
&lt;a href=&#34;https://uk.store.bambulab.com/products/p1s&#34;&gt;P1S&lt;/a&gt; — a printer generally regarded
as about as trouble free as you can get — to £522 including shipping, I splurged on one.&lt;/p&gt;
&lt;h3 id=&#34;concerns&#34;&gt;Concerns&lt;/h3&gt;
&lt;p&gt;I make it sound like that was an easy decision, but I actually ummed and ahed
a lot before pulling the trigger. I had a lot of objections to a Bambu Labs
printer, and wasn’t quite sure if the price made up for them.&lt;/p&gt;
&lt;!--more--&gt;
&lt;p&gt;The Bambu Labs printers are designed to be more consumer friendly than most
previous 3D printers. You can start a model printing directly from their
website or mobile app, which employs a cloud-based slicer that then sends the
model down to your printer. This is one of those things that sounds amazing for
consumers, but should terrify anyone who works with computers. There are so many
ways for a “cloud-connected” printer to go wrong, and with such potentially
disastrous results it gives me anxiety just thinking about it.&lt;/p&gt;
&lt;p&gt;This is not just theoretical, either. In a blog post with the rather under-stated
title of &lt;a href=&#34;https://blog.bambulab.com/cloud-temporary-outage-investigation/&#34;&gt;“Initial Investigation in the Bambu Cloud Temporary Outage”&lt;/a&gt;,
Bambu describe how a server-side error caused printers to start printing unprompted.
Users on Reddit reported printers starting in the middle of the night, trying
to print on top of other objects that were left on the build plate, and so on.
3D printers can easily start fires; having them cloud controlled is frankly
insane.&lt;/p&gt;
&lt;p&gt;As a result of that “temporary outage”, Bambu further improved the “LAN mode”
for their printers, which allows you to sever its connection from their servers
and control it over the network. That’s basically how my old printer worked
anyway, so I was happy enough that I could enable LAN mode and ignore all the
cloud nonsense.&lt;/p&gt;
&lt;p&gt;My other objection to Bambu was more nebulous. Almost all 3D printers are built
upon open source software, and some companies like Prusa go out of their way to
make their products more open to give back to the community. Bambu don’t really
do any of that: I’d be very surprised if they &lt;em&gt;hadn’t&lt;/em&gt; incorporated a bunch of
open source software into their printers and just ignored the licences&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:1&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;. I’d
much rather give my money to a company doing good, but the only equivalent
printer was the &lt;a href=&#34;https://www.prusa3d.com/product/original-prusa-mk4-2/&#34;&gt;Prusa MK4&lt;/a&gt;
which was &lt;em&gt;double&lt;/em&gt; the price and didn’t come with an enclosure. To add an
enclosure kit would have made it £800 more expensive than the P1S, and I just
can’t justify that.&lt;/p&gt;
&lt;h3 id=&#34;experience&#34;&gt;Experience&lt;/h3&gt;
&lt;p&gt;The initial setup of the P1S was a breeze. While the SV06 shipped in parts, the
P1S came fully assembled and just needed some packaging and retaining screws
removed. These were all clearly marked with large coloured arrows. It’s very
much designed so anyone can do it.&lt;/p&gt;
&lt;p&gt;I ran through the initial calibration, firmware update, and then put it
in LAN mode. Then I just started printing and it went fine. No messing around
levelling a bed, or using a sheet of paper to try and set the Z-offset properly;
it just worked. It was &lt;em&gt;really&lt;/em&gt; loud though. After talking a bit with a friend
who’d also bought a P1S, I realised that most of his noise was from the fans,
while most of mine was from the servos. A bit of research later and I found out
they added servo noise compensation in a firmware update, but I hadn’t run the
calibration for it because the initial calibration happened before the update.
After doing that, the printer went from sounding like it was impersonating a
continental police siren to just being a bit fan-y.&lt;/p&gt;
&lt;p&gt;The biggest difference over the SV06 is the speed. I’d swapped the nozzle on the
SV06 to a 0.6mm one, and generally printed at a layer height of 0.4mm. The P1S
still has its 0.4mm nozzle, and I mostly print at a layer height of 0.2mm. So
it’s laying down twice the number of layers, with a nozzle that can extrude
half the amount of plastic… And it’s at least twice as fast! Prints that I
previously wouldn’t have even attempted on the SV06 are now 8-10 hours on the
P1S, without even adjusting the speed or lowering the quality. Here’s a
side-by-side comparison:&lt;/p&gt;
&lt;figure class=&#34;image full&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/further-adventures-in-3d-printing/comparison.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/further-adventures-in-3d-printing/comparison.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/further-adventures-in-3d-printing/comparison.png&#34; alt=&#34;A side-by-side comparison of printing time on the P1S and SV06. The P1S prints 8 objects at higher quality than the SV06 prints 3.&#34; loading=&#34;lazy&#34; width=&#34;1856&#34; height=&#34;944&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;P1S vs SV06.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The P1S can print eight objects because of its slightly larger bed, while the
SV06 can only manage three. The P1S is also printing at a much higher quality.
Yet somehow it still ends up being faster. It’s mind blowing.&lt;/p&gt;
&lt;p&gt;The other big difference I’ve not mentioned yet is that the P1S is a “core XY”
printer rather than a “bed slinger”. That means in order to move along the
Y-axis (from the front of the bed to the back), the P1S moves the extruder
while the SV06 moves the bed. This makes a big difference when printing tall
objects, as they’re far more likely to wobble and come loose when the bed is
moving back and forth. It also presumably makes the kinematics a lot easier
as the mass of the moving parts is constant, instead of gradually becoming
heavier as more and more plastic gets added.&lt;/p&gt;
&lt;h3 id=&#34;the-only-failures&#34;&gt;The only failures&lt;/h3&gt;
&lt;p&gt;I’ve only had a handful of failures since getting the P1S. I had one weird
issue where the slicer decided to print some parts extremely slowly, and the
print just didn’t work out in those parts; that’s more of a slicer issue,
though: after tweaking the model to stop the weird behaviour it printed fine.&lt;/p&gt;
&lt;p&gt;The only failure I can directly attribute to the printer was when I tried
ironing. This is a process where after printing the top layer, the printer
slowly goes back over it at the same height while extruding a small amount of
plastic. This is meant to fill in the small gaps between extrusions and give
a smooth finish. In my case, though, it just clogged the nozzle and extruder.&lt;/p&gt;
&lt;p&gt;At first I thought it was because I was printing too hot: the profile I was
using was designed for fast printing, so had to melt the filament rapidly, but
there was no way to turn that temperature down just for the ironing pass. I
tried another run at a lower temperature and everything clogged again. I then
swore off ironing, did a normal print, and everything clogged once more.&lt;/p&gt;
&lt;p&gt;At this point I was getting exceedingly good at disassembling the hot end, but
also fairly annoyed. It felt like I was using an SV06 again&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:2&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;! I’d previously
read some recommendations that you should leave the door or top of the enclosure
open when printing PLA, but had never really had any issues so hadn’t bothered.
I tried a print with the top propped open and everything worked smoothly again.
In hindsight all these clogging issues happened when it was particularly hot
and humid outside, which was probably a factor.&lt;/p&gt;
&lt;h3 id=&#34;summary&#34;&gt;Summary&lt;/h3&gt;
&lt;p&gt;Upgrading to the P1S has made the 3D printer feel more like a tool and less like
an additional ongoing project. The speed increase continues to amaze me, as
does the increase in quality.&lt;/p&gt;
&lt;p&gt;I still think it was valuable to have the SV06 to learn how printers work — you
definitely don’t get as much of that from using something so consumer-focused
as the P1S — and figure out if I was actually going to use it long-term or
whether it was just a fad for me. So it served its purpose, but I’m rather
glad to see the back of it in favour of something better in pretty much every
way.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr/&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;And if they did there’s nothing anyone can do about it because copyright
doesn’t exist in the same way in China. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:1&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;
&lt;p&gt;Although taking apart the P1S (which is a proprietary machine you might
not expect to be very user-servicable) was orders of magnitude easier than taking
apart the SV06 (which was an open source design). Everything is just much nicer:
the connectors are easier to attach and detach, the screws are all a consistent
size instead of needing a whole suite of allen keys, and so on. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:2&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</content>
    </entry>
    <entry>
        <title>Project log: Filament weight display</title>
        <link href="https://chameth.com/filament-weight-display/"/>
        <updated>2023-12-03T00:00:00Z</updated>
        <id>https://chameth.com/filament-weight-display/</id>
        <content xml:lang="en" type="html">&lt;figure class=&#34;image left&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/filament-weight-display/finished.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/filament-weight-display/finished.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/filament-weight-display/finished.jpg&#34; alt=&#34;Filament weight display&#34; loading=&#34;lazy&#34; width=&#34;327&#34; height=&#34;247&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;The finished project&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;One problem&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:1&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; I have when 3D printing is that it’s hard to gauge whether
there’s enough filament left on a roll to complete a print. Sometimes it’s
obvious when the print is small or the roll is full, but often it’s not.
If I’m unsure about it, I end up obsessing over the printer instead of just
leaving it to do its thing.&lt;/p&gt;
&lt;p&gt;The typical approach to this problem is to use a run-out sensor, which stops
the printer when it detects that the filament is no longer running through it.
I’d rather know in advance though: it’s no good stopping the print if I’m
making something that has to look nice and now half of it is one colour and
the remainder will be something else.&lt;/p&gt;
&lt;p&gt;While browsing around for ideas, I came across a project that used a load cell
to measure the weight of the filament roll&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:2&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;. I didn’t really know what a
load cell was, but decided to do some research and see if I could build my own.
I didn’t actually read the article I found, intending to figure things out on
my own if I could.&lt;/p&gt;
&lt;!--more--&gt;
&lt;h3 id=&#34;load-cells-101&#34;&gt;Load Cells 101&lt;/h3&gt;
&lt;figure class=&#34;image right&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/filament-weight-display/loadcell.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/filament-weight-display/loadcell.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/filament-weight-display/loadcell.jpg&#34; alt=&#34;Product picture of a load cell and amplifier&#34; loading=&#34;lazy&#34; width=&#34;400&#34; height=&#34;400&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;A load cell, its amplifier, and some random headers&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;At the heart of the project is a load cell. It turns out these are the things
that do the lifting in most electronic scales. They’re basically a carefully
machined metal bar with a strain gauge attached to it. The strain gauge is
a long trace of conductive material that runs back and forth across the middle
of the bar. When the bar bends very slightly (due to load), the conductor flexes
with it and gets longer or shorter; that flex changes its resistance by a tiny
amount, which can be measured using a
&lt;a href=&#34;https://en.wikipedia.org/wiki/Wheatstone_bridge&#34;&gt;Wheatstone bridge&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The Wheatstone bridge is a diamond of resistors, and requires an ’excitation’
voltage to be applied across it in one direction, and the resistance is then
read from the perpendicular pair. As the resistance is tiny, and the changes
even more tiny, you generally need an amplifier to read it and output something
more useful. The HX711 is such an amplifier, and is so ubiquitous that most
places that sell load cells will just bundle one with it.&lt;/p&gt;
&lt;p&gt;The HX711 connects to the load cell using four wires: two for the excitation
voltage, and two for the output. On the other end it takes a 5V input, ground,
and then has two pins to deal with the onward communication: one for a clock
signal, and one for data. We’ll get back to that later!&lt;/p&gt;
&lt;h3 id=&#34;early-prototyping&#34;&gt;Early prototyping&lt;/h3&gt;
&lt;figure class=&#34;image left&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/filament-weight-display/prototype.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/filament-weight-display/prototype.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/filament-weight-display/prototype.jpg&#34; alt=&#34;Prototype weight sensor built on a breadboard&#34; loading=&#34;lazy&#34; width=&#34;500&#34; height=&#34;750&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;The prototype, mid-debugging&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I decided early on to base the project around an RP2040-based board, as I had
previous experience with them and they’re cheap and easy to get hold of. I
started off with a Raspberry Pi Pico, slotted it into a breadboard, and
connected it to the HX711. A short search produced three or so different
python libraries, but I couldn’t get any of them to work reliably.&lt;/p&gt;
&lt;p&gt;If you look at the picture of the prototype, you’ll notice a large bundle
of wires to the left. These are connected to a logic analyser I bought to
try and understand what on earth was happening, because it &lt;em&gt;seemed&lt;/em&gt; like
the code should work, and there was definitely some communication with the
HX711 happening, but none of the readings made any sense.&lt;/p&gt;
&lt;p&gt;At one point I noticed that moving my head near to the Pico caused the
readings to change, but I didn’t understand the implication of that&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:3&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt;.
I went to bed unable to figure out what was going on, and just before I dozed
off realised that I must have the pins configured wrong. The next day I checked
the spec sheet and discovered that the pin with the number “1” silk-screened
next to it is in fact GPIO 0, not GPIO 1&lt;sup id=&#34;fnref:4&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:4&#34; role=&#34;doc-noteref&#34;&gt;4&lt;/a&gt;&lt;/sup&gt;. The pin the Pico was trying to
read data from wasn’t actually connected to anything, which is why waving my
hand near it was enough to induce a signal.&lt;/p&gt;
&lt;p&gt;Updating the code to use the correct pins made everything work &lt;em&gt;much&lt;/em&gt; better
and I started to get sensible readings. At this point I came across the
&lt;a href=&#34;https://www.waveshare.com/rp2040-zero.htm?sku=20187&#34;&gt;RP2040-Zero by Waveshare&lt;/a&gt;,
which is not only a smaller, cuter, cheaper Pico, but has silk-screened numbers
that actually match the GPIO pin numbers (plus an actual a reset button!). I
immediately bought several, and switched the prototype over as soon as they
arrived.&lt;/p&gt;
&lt;p&gt;The rest of the prototyping was reasonably uneventful. I used a little OLED
screen I had sitting around from a past Ali Express order, and once again
there were several python libraries to handle it. Soon enough I had raw
weight readings from the load cell (which was stuck to my desk with painters’
tape) being displayed on the screen.&lt;/p&gt;
&lt;p&gt;The load cells don’t measure absolute weight, so you have to “calibrate” them
by remembering the reading when there is no load, and then applying a scaling
factor to turn the raw reading into a weight. I worked these out by putting
a known weight on the business end of the load cell, and then hard-coding the
numbers.&lt;/p&gt;
&lt;h3 id=&#34;an-interruption-to-deal-with-interrupts&#34;&gt;An interruption to deal with interrupts&lt;/h3&gt;
&lt;p&gt;One thing I noticed with the MicroPython libraries for the HX711 is that they
relied on polling the data pin to see if there was any data to read. The way
the HX711 communicates is by pulling the data line low when it has data; you
then have to pulse the clock line to get it to shift a bit of the reading out
over the data line. The HX711 I have operates at 10Hz by default&lt;sup id=&#34;fnref:5&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:5&#34; role=&#34;doc-noteref&#34;&gt;5&lt;/a&gt;&lt;/sup&gt;, and
reading the data takes almost no time at all, so the most “efficient” thing
to do is to sleep for 100ms between readings.&lt;/p&gt;
&lt;p&gt;This is fine if you’re not doing anything else, but gets awkward if you also
want to refresh a screen at certain points, or check for button inputs, etc.
It also upset me to see the data line being low for so long when viewing the
logic analyser traces. The RP2040 supports setting interrupts on the GPIO pins,
but I couldn’t find anyone who was actually using them for the HX711.&lt;/p&gt;
&lt;p&gt;I therefore did the only sensible thing: wrote my own library, using interrupts
to monitor for data availability. It was pretty straightforward, but didn’t
actually work consistently. Looking more closely at the other libraries, they
all do &lt;em&gt;something&lt;/em&gt; to try to increase the speed at which the clock signals are
written&lt;sup id=&#34;fnref:6&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:6&#34; role=&#34;doc-noteref&#34;&gt;6&lt;/a&gt;&lt;/sup&gt;: some were disabling interrupts, others were using assembly. I,
again, did the only sensible thing: I threw all the python away and wrote a new
driver in &lt;a href=&#34;https://tinygo.org/&#34;&gt;TinyGo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My new code is event driven: it adds interrupts for button presses, and one for
the HX711 data line. When the data line is pulled low, it removes that
interrupt (so it doesn’t fire for every bit of data we receive), does the
reading logic, then re-adds the interrupt. Theoretically if you spam buttons
at an inopportune time you could mess up a reading, but the reading 100ms after
would be fine. So my code spends most of the time idle, which translates to very
little power draw.&lt;/p&gt;
&lt;h3 id=&#34;user-experience&#34;&gt;User experience&lt;/h3&gt;
&lt;p&gt;While prototyping I was using some standard small push buttons to navigate the
user interface I was creating. I soon realised that if I needed to enter
weights on the real device I wasn’t going to be happy poking an annoying little
button hundreds of times, and I didn’t feel like coding anything more elaborate
to make input easier. Instead, I switched to using a rotary encoder. This is
a bit like a variable resistor, but it can rotate completely freely, and sends
a signal when it’s turned. It also has a button built in, so you can press it
down to select things.&lt;/p&gt;
&lt;p&gt;It took me a while to get the hang of the rotary encoder: it has two data pins
for turning, and you have to wait for a falling edge on one pin and then
immediately read the other to figure out which way it’s turning. My initial
attempts at doing this weren’t quite immediate enough&lt;sup id=&#34;fnref:7&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:7&#34; role=&#34;doc-noteref&#34;&gt;7&lt;/a&gt;&lt;/sup&gt;, so I ended up with
somewhat random data. Trying to select a number when going right sometimes
increases it and sometimes decreases it is an exercise in frustration.&lt;/p&gt;
&lt;p&gt;The correctly functioning rotary encoder makes it much easier to input numbers
and change settings. I ended up with a very simple menu system at the bottom
of the screen which lets you cycle between three options: setting the spool
weight, zeroing the scale, and calibrating it. Zeroing is the most
straightforward option: it simply reads the current value, and saves it as the
zero offset in the flash memory. Selecting spool weight enters a mode where
turning the rotary encoder increases or decreases the spool weight by a gram at
a time; pressing the encoder in saves the value to flash and exits the mode&lt;sup id=&#34;fnref:8&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:8&#34; role=&#34;doc-noteref&#34;&gt;8&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Calibration is a bit more confusing. Changing the spool weight changes the
value you see on the screen directly and predictably. Calibration instead
affects the “scale” value you can’t actually see; you just see the effect
of the scaling on the weight reading. Still, if you have a known weight on
the scale you simply rotate the encoder until the right weight is displayed
on the screen. Pressing the encoder saves the value to flash as with the other
modes.&lt;/p&gt;
&lt;h3 id=&#34;putting-it-together&#34;&gt;Putting it together&lt;/h3&gt;
&lt;figure class=&#34;image right&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/filament-weight-display/protoboard.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/filament-weight-display/protoboard.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/filament-weight-display/protoboard.jpg&#34; alt=&#34;Components mounted onto a protoboard&#34; loading=&#34;lazy&#34; width=&#34;500&#34; height=&#34;626&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;The final assembly&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The final step was to assemble everything in a way where it would be usable,
and wires wouldn’t keep falling out whenever you looked at it. I found some
cute little prototype boards that would just about fit all the components on –
I left the OLED screen and rotary encoder off so they could be mounted to the
front of the box.&lt;/p&gt;
&lt;p&gt;I obviously 3D printed the enclosure, because I own a 3D printer and therefore
spend a lot of my time 3D printing parts for my 3D printer. In my first prototype I
put raised areas on the back panel and put some heat-press inserts in them,
intending to screw down the protoboard. The screw holes I was going to use,
however, were underneath the soldered-down RP2040-Zero and HX711. I instead
uninserted the inserts and simply put some screws through the back panel with
the intention of slotting nuts under the components. This turned out to be both
incredibly annoying and entirely unnecessary; the board sits at the back of the
box on its own, and there’s no real danger from it having a bit of freedom.&lt;/p&gt;
&lt;p&gt;While I was prototyping I was powering the circuit from the USB port on the
microcontroller, but for the actual unit I added a buck converter and ran a
cable from the 3D printer’s power supply (which outputs 24V). This has the
handy side effect of turning the scale on and off with the printer. I’ve
recently bought a bench power supply, which made testing this part a lot easier:
being able to dial in a voltage and current limit and then just plug things in
is great.&lt;/p&gt;
&lt;p&gt;The other thing that needed to change between my prototype and the final version
was the load cell. It needed to be attached to the printer in such a way that one
end bore all the weight of the spool (and, you know, not stuck to my desk with
painters’ tape). I was originally going to do something fancy with bearings,
but settled on the easier option of a spool holder which uses the same threading
as the original, so it slots straight in:&lt;/p&gt;
&lt;figure class=&#34;image full&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/filament-weight-display/spoolmount.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/filament-weight-display/spoolmount.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/filament-weight-display/spoolmount.jpg&#34; alt=&#34;3D printed spool holder with embedded load cell&#34; loading=&#34;lazy&#34; width=&#34;1000&#34; height=&#34;738&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;The load cell mounted as part of a spool holder on the printer.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;With all of this done, I invented a complicated mounting system to keep it
attached to the printer. Any resemblance to blue painters’ tape is entirely
coincidental.&lt;/p&gt;
&lt;h3 id=&#34;bill-of-materials-and-sources&#34;&gt;Bill of materials and sources&lt;/h3&gt;
&lt;p&gt;Here are the major components that I ended up using:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RP2040-Zero microcontroller&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://www.waveshare.com/rp2040-zero.htm?sku=20187&#34;&gt;WaveShare&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;£3.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5KG load cell + HX711&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://www.aliexpress.com/item/1005005990833147.html&#34;&gt;AliExpress&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;£1.89&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Buck converter&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://www.aliexpress.com/item/32832061095.html&#34;&gt;AliExpress&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;£0.51&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SH1106 OLED module&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://www.aliexpress.com/item/1005005967766159.html&#34;&gt;AliExpress&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;£1.70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rotary encoder&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://www.aliexpress.com/item/1005005973850924.html&#34;&gt;AliExpress&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;£0.80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mini PCB prototype board&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://www.amazon.co.uk/dp/B09X1DMSYZ&#34;&gt;Amazon&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;£1.49&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Where things come in packs of more than one, I’ve listed the cost of one unit.
I’ve not included things like wires, connectors, or the filament used in
printing the enclosure as you can use whatever you have to hand.&lt;/p&gt;
&lt;p&gt;The source code I wrote is &lt;a href=&#34;https://github.com/csmith/gorp2040&#34;&gt;available on GitHub&lt;/a&gt;,
and the designs for the 3D printed parts are &lt;a href=&#34;https://cad.onshape.com/documents/ccd44795b969ab9fcd0a9722/w/753c45e468de6247c6c31aa9/e/a7a698a0dc8a49767764053f?renderMode=0&amp;amp;uiState=656d12a3f305150149b205cc&#34;&gt;shared in OnShape&lt;/a&gt;.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr/&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;Among many, many others, of course. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:1&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;
&lt;p&gt;I can’t actually find where I saw this original project now, but there
are quite a few kicking around if you search. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:2&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:3&#34;&gt;
&lt;p&gt;This happened at about 1am and I was tired and frustrated. That’s my
excuse, anyway. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:3&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:4&#34;&gt;
&lt;p&gt;At this point I realised I’d made the exact same mistake the last time I
used a Pico. Fool me once, etc. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:4&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:5&#34;&gt;
&lt;p&gt;You can change this to 80Hz by resoldering a resistor on the board, but
10Hz is already several orders of magnitude faster than I actually need. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:5&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:6&#34;&gt;
&lt;p&gt;If the clock signal stays high for longer than 60ms it’s treated as
a shutdown signal by the HX711. This is somewhat unideal when you’re
trying to read data from it. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:6&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:7&#34;&gt;
&lt;p&gt;Because of my very clever interrupt system. I wanted to do as little work
in the interrupt handler as possible, so I was just enqueuing an event
to be processed in the main loop. This introduced enough delay that I
wasn’t reading the second pin at the right time. The solution was to read
the pin in the interrupt handler and then just enqueue a “left” or “right”
event instead of my original “turning somehow, you figure it out” event. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:7&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:8&#34;&gt;
&lt;p&gt;I was initially planning on hard-coding the weights for all the spools
I’m likely to use, and just having a way to switch between them. But the
thought of having to re-flash the device every time I bought a new type
of filament made me reconsider. Plus, it was way easier to not do that. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:8&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</content>
    </entry>
    <entry>
        <title>Adventures in 3D printing</title>
        <link href="https://chameth.com/adventures-in-3d-printing/"/>
        <updated>2023-09-17T00:00:00Z</updated>
        <id>https://chameth.com/adventures-in-3d-printing/</id>
        <content xml:lang="en" type="html">&lt;figure class=&#34;image right&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/adventures-in-3d-printing/sv06.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/adventures-in-3d-printing/sv06.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/adventures-in-3d-printing/sv06.jpg&#34; alt=&#34;Marketing image of the Sovol SV06&#34; loading=&#34;lazy&#34; width=&#34;500&#34; height=&#34;551&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;The Sovol SV06.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I’d been idly considering getting a 3D printer for a while, but have only
recently taken the plunge. I picked up a &lt;a href=&#34;https://sovol3d.com/products/sovol-sv06-best-budget-3d-printer-for-beginner&#34;&gt;Sovol SV06&lt;/a&gt;
from Amazon for £199.99&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:1&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;, which is a model commonly recommended for beginners.
About three weeks later, I think I’ve finally finished fixing all the problems
the printer has, and thought I’d document them.&lt;/p&gt;
&lt;h3 id=&#34;setup-and-out-of-the-box-performance&#34;&gt;Setup and out of the box performance&lt;/h3&gt;
&lt;p&gt;The setup of the printer itself was straight forward. Most parts are
assembled, you just have to bolt the frame together, bolt the various parts to
the frame, and connect some wires. The hex bits in the
&lt;a href=&#34;https://store.ifixit.co.uk/products/mako-driver-kit-64-precision-bits&#34;&gt;iFixit Mako Bit Set&lt;/a&gt;
were a godsend for this, as the included hex keys were a bit flimsy.&lt;/p&gt;
&lt;!--more--&gt;
&lt;p&gt;Once everything is bolted and connected, you turn it on and immediately start
looking around for the jet engine you can hear. The fans are &lt;em&gt;loud&lt;/em&gt;. The
instructions walk you through the initial calibration, which consists of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Levelling the X-Axis gantry (where the print head moves). The printer does
this by (gently) ramming the gantry up as far as it will go.&lt;/li&gt;
&lt;li&gt;Adjusting the Z-offset of the print head. The printer has an inductive
proximity sensor attached to the extruder, but it doesn’t know how high it is
relative to the nozzle. The calibration process involves manually dialing in
this offset while moving a piece of paper back and forth under the nozzle.&lt;/li&gt;
&lt;li&gt;Levelling the bed. This is entirely automatic: the printer just probes a bunch
of points on the bed with the proximity sensor and builds up a mesh it can use
to compensate for the bed not being level.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After that’s done, the included SD card has the standard “Benchy” model on it,
pre-sliced and ready to print. It came out looking good:&lt;/p&gt;
&lt;figure class=&#34;image full&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/adventures-in-3d-printing/benchy.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/adventures-in-3d-printing/benchy.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/adventures-in-3d-printing/benchy.jpg&#34; alt=&#34;A white &amp;#39;Benchy&amp;#39; sat on the 3D printer bed&#34; loading=&#34;lazy&#34; width=&#34;1300&#34; height=&#34;975&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;A nice, problem free Benchy. No dramatic irony here.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3 id=&#34;the-noise-oh-god-the-noise&#34;&gt;The noise. Oh god, the noise.&lt;/h3&gt;
&lt;p&gt;I set about printing a few more things, but the noise of the machine quickly
got to me. It sits approximately 80cm from my ear, and between the fans and the
noises it makes when moving it was driving me mad. The fan in the PSU seems to
sit at 100% power regardless of temperature or load, and sounds about three
times louder than my laptop’s fans do when they’re running at full tilt.
Fortunately, someone had designed a printable &lt;a href=&#34;https://www.printables.com/model/514699-sovol-sv06-psu-fan-silencer&#34;&gt;PSU fan silencer&lt;/a&gt;
that redirects the airflow (and most of the noise) from the fan. It doesn’t feel
like it should help that much, but it does significantly reduce the noise as it
claims. This was the first of many times I’d use the 3D printer to print parts
for itself.&lt;/p&gt;
&lt;p&gt;The other noise was a lot less straightforward. The linear rods that all three
axes run on pass through several bearings. Sovol apparently don’t bother to
apply any lubrication to these, so it sounds like an elephant is moving through
a gravel pit when the printer is running. Some people online suggest rubbing
lube on the rods and pushing the movable parts back and forth, but this just
results in a load of lube caught on the dust filters of the bearings&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:2&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;The solution to this, then, was to dismantle most of the printer to gain access
to each of the ten bearings, and manually lubricate them. This was tedious and
messy, but not actually that difficult. It doesn’t seem like the kind of thing
you should have to do on a brand-new machine, but I guess you get what you pay
for.&lt;/p&gt;
&lt;h3 id=&#34;speeeeeeed&#34;&gt;Speeeeeeed&lt;/h3&gt;
&lt;p&gt;3D printers are slow. The SV06 is not one of the faster ones. One simple way
to increase the print speed is to change out the nozzle. The SV06 comes with a
0.4mm nozzle, which is pretty standard, but if you upgrade to a 0.6mm nozzle you
can extrude more than twice the volume of plastic in the same amount of time&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:3&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt;.
The consensus among anonymous internet people is that if you’re using a modern
slicer there isn’t much difference in print quality when doing this.&lt;/p&gt;
&lt;p&gt;I dutifully ordered a new nozzle, and after it turned up set about installing
it. This is mostly straight forward, aside from the complication that you’re
meant to do it when the hot-end is hot. That makes sense: you want it be under
the same thermal expansion it will be under during use, but the tool they
provide to change the nozzle is all metal. So you have a relatively delicate
operation that you have to do in a bit of a rush before the heat creeps all
the way up to your hand.&lt;/p&gt;
&lt;p&gt;With the new nozzle in place, the printer was noticeably faster. I didn’t notice
any difference in print quality, either.&lt;/p&gt;
&lt;h3 id=&#34;sd-cards-are-not-fun&#34;&gt;SD cards are not fun&lt;/h3&gt;
&lt;p&gt;Up until this point, my process for printing anything was to slice it on the
computer, export the gcode file to an SD card, turn around and walk over to
the printer, insert the SD card, and start the print from the LCD. I don’t
particularly mind those steps, but the SD card slot on the Sovol is &lt;em&gt;terrible&lt;/em&gt;.
It doesn’t line up well to the hole in the case, doesn’t guide the card in
well, and is all round a pain to use.&lt;sup id=&#34;fnref:4&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:4&#34; role=&#34;doc-noteref&#34;&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;To remove the sneaker-net element of this process, I ordered a Raspberry Pi and
installed &lt;a href=&#34;https://octoprint.org/&#34;&gt;OctoPrint&lt;/a&gt; on it. OctoPrint provides a web
interface to the printer (which it controls over USB), and most slicers can
automatically export the gcode to an OctoPrint instance.&lt;/p&gt;
&lt;p&gt;While OctoPrint itself is great, there was one small problem: if you connect
anything to the USB port on the printer, it tries to draw 5V from it and
“backpower” itself. It’s not &lt;em&gt;meant&lt;/em&gt; to draw power from the USB port, it’s just
not designed well. A lot of the cheaper 3D printers share this flaw, it seems.
The fix here is to get a power blocker such as the one made by
&lt;a href=&#34;https://portablepowersupplies.co.uk/product/usb-power-blocker&#34;&gt;PortaPow&lt;/a&gt;
that simply leaves the +5V line unconnected between the two ends. Presumably
Sovol could fix this by adding a diode to the USB power line, but maybe that
would push their costs too high?&lt;/p&gt;
&lt;p&gt;After getting OctoPrint working, I printed a nice case for it to mount on the
side of the printer. I slotted the Pi in, and when I powered it on again it
didn’t boot. When I went to pick it up to debug the problem, I burnt my finger
on the SD card&lt;sup id=&#34;fnref:5&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:5&#34; role=&#34;doc-noteref&#34;&gt;5&lt;/a&gt;&lt;/sup&gt;. Apparently if you break SD cards in a certain way, they can
short out and get &lt;em&gt;very&lt;/em&gt; hot. Fun! Maybe I shouldn’t have been using the
no-brand card that came with the printer…&lt;/p&gt;
&lt;h3 id=&#34;what-do-we-do-with-a-drunken-bed&#34;&gt;What do we do with a drunken bed?&lt;/h3&gt;
&lt;p&gt;I continued printing bits and bobs, but was starting to notice a new issue:
if I was printing multiple copies of the same object, those on the right
hand side of the bed often failed. OctoPrint has a nifty plugin that lets you
cancel certain regions from the build, which saves it ruining everything, but
only being able to print in certain places was annoying.&lt;/p&gt;
&lt;p&gt;I ran through all the troubleshooting steps I could think of, including a
foray into custom firmware with different bed levelling abilities, but nothing
seemed to help. Another OctoPrint plugin later and I could see the problem:&lt;/p&gt;
&lt;figure class=&#34;image full&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/adventures-in-3d-printing/bed1.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/adventures-in-3d-printing/bed1.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/adventures-in-3d-printing/bed1.png&#34; alt=&#34;A visualisation of the bed mesh, showing a variation of 0.5mm.&#34; loading=&#34;lazy&#34; width=&#34;788&#34; height=&#34;450&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;This should probably be straight.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The orientation of the graphic is a bit confusing, but you can see how the
right hand side of the bed is significantly higher than the left. There’s a
total variance of just over 0.5mm, which is apparently enough to cause problems
even with the auto bed levelling. I spoke to Sovol support, and eventually they
suggested disassembling the print bed and measuring the aluminium spacers that
hold the bed. Sure enough, they were all different heights:&lt;/p&gt;
&lt;figure class=&#34;image full&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/adventures-in-3d-printing/spacers.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/adventures-in-3d-printing/spacers.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/adventures-in-3d-printing/spacers.jpg&#34; alt=&#34;Five aluminium spacers, each a different height.&#34; loading=&#34;lazy&#34; width=&#34;750&#34; height=&#34;200&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;You had one job…&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;There’s a 0.13mm variance in their heights, which doesn’t account for the 0.5mm
seen in the bed mesh, but it’s a good starting point. Rather than go back and
forth with Sovol support over the course of many days, I just ordered some
nylock nuts and reinstalled the bed without spacers. The nuts hold the bed in
place, while still allowing you to tighten or loosen the screws to adjust the
height.&lt;/p&gt;
&lt;p&gt;After five or six rounds of totally and utterly uneventful adjusting and
re-leveling, I got to this point:&lt;/p&gt;
&lt;figure class=&#34;image full&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/adventures-in-3d-printing/bed2.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/adventures-in-3d-printing/bed2.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/adventures-in-3d-printing/bed2.png&#34; alt=&#34;A visualisation of the bed mesh, showing a variation of 0.1mm.&#34; loading=&#34;lazy&#34; width=&#34;931&#34; height=&#34;450&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;This is much more straight.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;There’s only so much you can do when you only have five screws to adjust, but
I’m pretty happy the variance is now under 0.15mm. That’s definitely in the
“good enough” range that the bed leveling algorithm can compensate for.&lt;/p&gt;
&lt;h3 id=&#34;whoops&#34;&gt;Whoops&lt;/h3&gt;
&lt;p&gt;So, about that “totally and utterly uneventful adjusting” I did. To get at the
screws that hold the bed in place, you have to remove the thin sheet that acts
as the build surface. This attaches magnetically to the bed, so it’s easy to
do, but I am lazy and decided to leave it off while I fiddled with the bed.&lt;/p&gt;
&lt;p&gt;There’s a slight problem here, though: when it’s working out the bed mesh the
printer uses the inductive proximity sensor. The inductive proximity sensor
detects metal. The metal is in the removable sheet. The first time I tried
to run the bed mesh, the printer rammed the extruder assembly into the bed at a
decent speed and ended up at a rather nasty angle.&lt;/p&gt;
&lt;p&gt;I checked over the extruder and found the heat break had bent:&lt;/p&gt;
&lt;figure class=&#34;image full&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/adventures-in-3d-printing/hotend.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/adventures-in-3d-printing/hotend.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/adventures-in-3d-printing/hotend.jpg&#34; alt=&#34;A heat break that has bent.&#34; loading=&#34;lazy&#34; width=&#34;353&#34; height=&#34;489&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;This should also probably be straight.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I tried printing with it as-is, but the filament came out too thin and at
an angle, which made the print go about as well as you’d expect. I ordered
a replacement, installed it, and… nothing worked still.&lt;/p&gt;
&lt;p&gt;At this point I was getting thoroughly fed up. There seemed to be one problem
after another, and no good way to diagnose problems with the print not sticking
(because there are a dozen or so different potential causes). After a lot of
frustrated googling, I found a post on the Prusa forums that suggested cleaning
the build sheet with dish soap. I’d cleaned it with IPA, but several people
were saying that dish soap magically fixed issues that IPA couldn’t. I took
the plate to the kitchen, gave it a wash, and the next print went down
perfectly.&lt;/p&gt;
&lt;h3 id=&#34;growing-a-backbone&#34;&gt;Growing a backbone&lt;/h3&gt;
&lt;p&gt;For a blissful period of about four days the printer was chugging along
perfectly with no issues. Then it started making a weird noise. In a display
of amazing restraint, I didn’t immediately throw the whole thing out the window.
Looking at the printer, I could see that the cable running from the extruder
assembly to the main board was sagging and rubbing on top of the print as the
print head was moving around. I’m not sure what property of the cable was
keeping it aloft before, but it apparently ran out after about three weeks.&lt;/p&gt;
&lt;p&gt;Like most of the other problems with the SV06, this design flaw has been noted
by others and worked around. I printed a &lt;a href=&#34;https://www.printables.com/model/447467-cable-chain-spine-for-sovol-sv06-3d-printer-extrud&#34;&gt;cable chain&lt;/a&gt;
and installed it&lt;sup id=&#34;fnref:6&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:6&#34; role=&#34;doc-noteref&#34;&gt;6&lt;/a&gt;&lt;/sup&gt;. Amusingly, while I was printing the chain, the cable dragged
on the print and dislodged one of the pieces entirely before it was finished.
Fortunately, it didn’t affect the neighbouring pieces, so I just had to run off
a replacement for that one.&lt;/p&gt;
&lt;p&gt;The difference is rather pronounced:&lt;/p&gt;
&lt;figure class=&#34;image full&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/adventures-in-3d-printing/backbone.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/adventures-in-3d-printing/backbone.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/adventures-in-3d-printing/backbone.jpg&#34; alt=&#34;A sagging cable, and a reinforced cable.&#34; loading=&#34;lazy&#34; width=&#34;844&#34; height=&#34;500&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;It feels like I own a 3D printer to print parts for my 3D printer…&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;If you can’t see it: in the first image the cable is sagging and actually
ends up below the print bed. The individual links of the cable chain limit
its freedom of movement, stopping it sagging.&lt;/p&gt;
&lt;h3 id=&#34;the-end&#34;&gt;The end?&lt;/h3&gt;
&lt;p&gt;Once again the printer is printing properly again. I’m sure there will be
more problems, but hopefully I get at least a week or so before anything else
goes wrong. Maybe I’ll print an “X days since the last SV06 problem” counter…&lt;/p&gt;
&lt;p&gt;I think, if I had to do this all again, but with the knowledge I have now, I’d
have stumped up the extra cash to get a &lt;a href=&#34;https://www.prusa3d.com/category/original-prusa-i3-mk3s/&#34;&gt;Prusa MK3S+&lt;/a&gt;&lt;sup id=&#34;fnref:7&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:7&#34; role=&#34;doc-noteref&#34;&gt;7&lt;/a&gt;&lt;/sup&gt;.
This is the original machine on which the SV06 (and a lot of other cheap imitations)
is based. Some obvious benefits it has over the SV06:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The extruder cable runs on the outside of the machine, so won’t droop onto
prints&lt;/li&gt;
&lt;li&gt;The heated bed has copper embedded in it for use in the induction sensor,
so it won’t crash into the bed if the build sheet is removed&lt;/li&gt;
&lt;li&gt;They actually put lubricant in the bearings instead of shipping them bone
dry&lt;/li&gt;
&lt;li&gt;The bed is held in place with screws instead of poorly sized aluminium
spacers, so you can adjust the height without having to take the whole thing apart&lt;/li&gt;
&lt;li&gt;They don’t use the cheapest possible power supply and main board, so it
doesn’t backpower off of USB devices or have the loudest fans known to mankind&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Prusa also have a much better attitude towards 3D printing: they open source the
hardware designs (which is why there are so many cheap clones), and they actually
use the printers themselves. The plastic parts of an SV06 are injection moulded,
but those on a Prusa printer are actually printed on a Prusa printer. Not only
is it cool that they use their own products, it means they have to run them
en-masse so they’re obviously invested in making them reliable. They also have
a reputation for exceptional support.&lt;/p&gt;
&lt;p&gt;Despite all the problems, I do enjoy having the printer. I’ll probably talk more
about the prints I’ve done that weren’t for the 3D printer itself in the future.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr/&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;The RRP is £279.99, Amazon discounted it down to £269.99 then offered a
£70 voucher. The voucher says it expires at midnight, but it’s said that every
day for the past three weeks… &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:1&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;
&lt;p&gt;Which is exactly what &lt;em&gt;another&lt;/em&gt; set of people said would happen. It was
worth a try, though. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:2&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:3&#34;&gt;
&lt;p&gt;This feels strange, but if you work out the areas of the nozzles it makes
sense: a 0.4mm diameter nozzle has an area of 0.04πmm² (the radius is half the
diameter, and the area of a circle is πr²), and a 0.6mm diameter nozzle has an
area of 0.09πmm². Maths! &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:3&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:4&#34;&gt;
&lt;p&gt;At least it doesn’t eject the SD card across the room like
&lt;a href=&#34;https://www.reddit.com/r/3Dprinting/comments/161wss8/ender_3_v2_yeeting_sd_card_across_the_room/&#34;&gt;this Ender 3 V2&lt;/a&gt;! &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:4&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:5&#34;&gt;
&lt;p&gt;My only 3D printer related injury so far. It’s impressive I can be around
a 200 degree hot-end and somehow burn myself on a storage device. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:5&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:6&#34;&gt;
&lt;p&gt;Well actually I printed 5 copies of their calibration piece until it was
perfect, then printed the actual cable chain, then had to sand each of the
individual links because they &lt;em&gt;still&lt;/em&gt; weren’t good enough. God, I hate sanding. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:6&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:7&#34;&gt;
&lt;p&gt;Or maybe I’d cave and get the newer, shinier, much more expensive MK4.
That wouldn’t be out of character. No, I didn’t just check my bank balance.
You’re imagining things. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:7&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</content>
    </entry>
</feed>
