<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="http://www.declarativesystems.com/feed.xml" rel="self" type="application/atom+xml" /><link href="http://www.declarativesystems.com/" rel="alternate" type="text/html" /><updated>2026-07-18T07:42:57+00:00</updated><id>http://www.declarativesystems.com/feed.xml</id><title type="html">Geoff Williams Blog</title><subtitle>Random tech, projects and fun stuff I don&apos;t want to forget</subtitle><author><name>Geoff Williams</name><email>geoff@declarativesystems.com</email></author><entry><title type="html">Growing VirtualBox disks (Linux)</title><link href="http://www.declarativesystems.com/2026/07/18/growing-virtualbox-disks.html" rel="alternate" type="text/html" title="Growing VirtualBox disks (Linux)" /><published>2026-07-18T00:00:00+00:00</published><updated>2026-07-18T00:00:00+00:00</updated><id>http://www.declarativesystems.com/2026/07/18/growing-virtualbox-disks</id><content type="html" xml:base="http://www.declarativesystems.com/2026/07/18/growing-virtualbox-disks.html"><![CDATA[<p>VirtualBox makes the perfect AI playground, but sooner or later you will likely hit the dreaded <code class="language-plaintext highlighter-rouge">No space left on device</code> error.</p>

<p>Here’s how to fix it:</p>

<h2 id="step-1---shutdown-vm">Step 1 - shutdown VM</h2>

<p>Needs to be powered off, not suspended</p>

<h2 id="step-2---grow-the-disk-with-vboxmanage">Step 2 - Grow the disk with <code class="language-plaintext highlighter-rouge">VBoxManage</code></h2>

<p>Find the VDI, then use <code class="language-plaintext highlighter-rouge">VBoxManage</code> to resize it (Example: grow to 50GB):</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># megabytes</span>
VBoxManage modifymedium disk debian<span class="se">\ </span>13.vdi <span class="nt">--resize</span> 51200
</code></pre></div></div>

<p>Output:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0%...10%...20%...30%...40%...50%...60%...70%...80%...90%...100%
</code></pre></div></div>

<p>This of course assumes you are using dynamically allocated disk images. If you opted for fixed image sizes, you will need to clone the whole drive to a new, growable image first as the command above would just error out in this case.</p>

<h2 id="step-3---grow-the-disk-in-linux">Step 3 - Grow the disk in Linux</h2>

<h3 id="warning-on-snapshots">Warning on snapshots</h3>
<p>Resizing a disk that has snapshots can behave unexpectedly (the resize applies to the base image, not the current state). Delete/merge your snapshots first - you have been warned.</p>

<h3 id="lvm">LVM</h3>
<p>If you used LVM when setting up your VM, you can follow my notes to <a href="/2023/07/23/growing-ext4-lvm-filesystem-to-use-all-free-space.html">grow the LV holding your root filesystem</a>. Job Done.</p>

<h3 id="growpart">growpart</h3>
<p>If you’re not using LVM and are also “lucky”, you can use <code class="language-plaintext highlighter-rouge">growpart</code> to expand a single <code class="language-plaintext highlighter-rouge">EXT2/3/4</code> partition, like this (see also <code class="language-plaintext highlighter-rouge">xfs_growfs</code> for XFS):</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install </span>cloud-guest-utils
<span class="nb">sudo </span>growpart /dev/sda 2
<span class="nb">sudo </span>resize2fs /dev/sda2
</code></pre></div></div>

<p>However, this relies on contiguous free space. If you have other partitions “in the way”, you will need to move them to the start/end of the drive with <code class="language-plaintext highlighter-rouge">gparted</code> first. Read on.</p>

<h3 id="gparted">gparted</h3>

<p>You can’t move the partitions you need to with <code class="language-plaintext highlighter-rouge">gparted</code> from the running system - you will need to boot into live OS and run <code class="language-plaintext highlighter-rouge">gparted</code> against the unmounted drive instead.</p>

<p>The easiest live environment I have found is <a href="https://ubuntu.com/download/desktop">Ubuntu Desktop</a>. Boot into the live environment and gparted will be right there as an app.</p>

<p>To boot Ubuntu Live DVD instead of your normal Linux:</p>
<ol>
  <li>Download the <code class="language-plaintext highlighter-rouge">.iso</code></li>
  <li>In VirtualBox, choose <code class="language-plaintext highlighter-rouge">VM Settings</code> -&gt; <code class="language-plaintext highlighter-rouge">Storage</code>, then insert the <code class="language-plaintext highlighter-rouge">.iso</code> into virtual CD drive (add one if missing). Don’t check the <code class="language-plaintext highlighter-rouge">Live CD</code> box as this will prevent ejecting the CD at the end of the process</li>
  <li>Boot order in same settings dialog should boot the CD first by default. If not, fix this now</li>
  <li>Boot the VM, <code class="language-plaintext highlighter-rouge">Try or Install Ubuntu</code>, wait a few mins for the OS to load, errors about unsupported graphics can <em>probably</em> (but not necessarily) be ignored, as long as things are still moving on the boot screen</li>
  <li>Click <code class="language-plaintext highlighter-rouge">Next</code> to select Language, Keyboard, Network (wired since it’s a VM), etc until you get to <code class="language-plaintext highlighter-rouge">What do you want to do with Ubuntu?</code>. Change the selection to <code class="language-plaintext highlighter-rouge">Try Ubuntu</code> and click <code class="language-plaintext highlighter-rouge">Close</code></li>
  <li>Now you should be dropped to the desktop. Click the funny 3 dot circle (app grid) in the bottom left of the screen to bring up the apps menu, start typing <code class="language-plaintext highlighter-rouge">gparted</code> click the app icon and then authorize running as root by clicking <code class="language-plaintext highlighter-rouge">Authenticate</code> on the next dialogue</li>
  <li>Good old <code class="language-plaintext highlighter-rouge">gparted</code> should now be running. Make sure to select the correct device, then move and adjust your filesystems as required to use the new space. If you have a <code class="language-plaintext highlighter-rouge">swap</code> partition, you may need to right click -&gt; <code class="language-plaintext highlighter-rouge">swapoff</code> it, to avoid the live CD sneakily using it and stopping you resizing things.</li>
  <li>Finish up in <code class="language-plaintext highlighter-rouge">gparted</code>, then shutdown the system with the menu in top right corner. The CD will automatically be ejected when you hit <code class="language-plaintext highlighter-rouge">enter</code> as prompted</li>
  <li>Power-on the VM, login and check you have some free space with the <code class="language-plaintext highlighter-rouge">df</code> command.</li>
</ol>

<p>Final word of warning: If <code class="language-plaintext highlighter-rouge">gparted</code> offers to fix the GPT layout, say yes.</p>

<p><em>And now back to our AI enhanced sandbox.</em></p>]]></content><author><name>Geoff Williams</name><email>geoff@declarativesystems.com</email></author><summary type="html"><![CDATA[VirtualBox makes the perfect AI playground, but sooner or later you will likely hit the dreaded No space left on device error.]]></summary></entry><entry><title type="html">Java and Maven - NVD and SBOM</title><link href="http://www.declarativesystems.com/2026/07/18/java-maven-nvd-sbom.html" rel="alternate" type="text/html" title="Java and Maven - NVD and SBOM" /><published>2026-07-18T00:00:00+00:00</published><updated>2026-07-18T00:00:00+00:00</updated><id>http://www.declarativesystems.com/2026/07/18/java-maven-nvd-sbom</id><content type="html" xml:base="http://www.declarativesystems.com/2026/07/18/java-maven-nvd-sbom.html"><![CDATA[<h2 id="tldr">TL;DR</h2>
<p>Maven NVD scanning is broken due to NIST NVD infrastructure, build an SBOM instead and gate the build with Grype or some other tool/process.</p>

<h2 id="q-why-was-i-up-all-night-yesterday">Q: Why was I up all night yesterday?</h2>

<ul>
  <li>A1: Maven</li>
  <li>A2: I wasted hours of my day trying to scan my code with <a href="https://dependency-check.github.io/DependencyCheck/dependency-check-maven/index.html"><code class="language-plaintext highlighter-rouge">dependency-check-maven</code></a></li>
</ul>

<h2 id="q-what-is-dependency-check-maven">Q: What is <code class="language-plaintext highlighter-rouge">dependency-check-maven</code></h2>

<ul>
  <li>A: It’s a Maven plugin to scan for dependencies with <a href="https://en.wikipedia.org/wiki/Common_Vulnerabilities_and_Exposures">CVEs</a>, with the power to fail builds if severity is high enough</li>
</ul>

<h2 id="nist-nvd-service---outage">NIST NVD Service - outage</h2>

<p>All joking aside, at least yesterday, the <a href="https://nvd.nist.gov/">NIST NVD service</a> <code class="language-plaintext highlighter-rouge">dependency-check-maven</code> relies on is just hopelessly broken. Downloading without an API key takes <em>hours</em>, if it works at all.</p>

<p>Yesterday, only approx 15% of the database was downloaded after a couple of hours.</p>

<p>At this point, I caved and <a href="https://nvd.nist.gov/developers/request-an-api-key">requested an API key</a>. Even this process took about half an hour, with multiple failures before a working API key was released.</p>

<p>I’ll save you the trouble of fighting for an API key: On my workstation at least, the database downloaded just as slowly as without a key at all. In fact, leaving Maven trying to download the NVD database with a valid key resulted in a <code class="language-plaintext highlighter-rouge">50x</code> error from the server after an hour or so, with the same result after several attempts.</p>

<p>Best-practice is to <a href="https://dependency-check.github.io/DependencyCheck/data/mirrornvd.html">setup a local (corporate) mirror of NVD</a>, however:</p>
<ul>
  <li>There are no actual instructions on how to do so, just a rough outline of the components needed</li>
  <li>Even if there were, it relies on upstream NVD server working for the initial download</li>
</ul>

<p>I’m pretty sure that for every team who diligently figures out correct mirroring, there’s another ten who just let their CI servers rip on the NVD upstream servers 24/7 for git pushes, so here we are with an NVD system that’s under so much load it’s unusable.</p>

<p>In reality, few developers will fight through all of this to scan their dependencies for CVEs, so the outcome is skipped tests and/or permanently removed security checks from build pipelines.</p>

<h2 id="solution-sbom">Solution? SBOM</h2>

<p>This is not an isolated outage, the NVD service has been failing for a while now. To prevent Maven build failures, there’s really no option but to disable/remove the plugin unless you’re hosting your own NVD mirror.</p>

<p>So what do we do instead? Many developers are turning to Software Bill of Materials - <a href="https://www.ibm.com/think/topics/sbom">SBOM</a>.</p>

<p>In the case of a Java Maven app, this means capturing the full dependency tree. There’s a plugin for this: <a href="https://github.com/CycloneDX/cyclonedx-maven-plugin"><code class="language-plaintext highlighter-rouge">cyclonedx-maven-plugin</code></a>.</p>

<p>Adding the plugin to your Maven project results in capturing dependency information as JSON and XML under the <code class="language-plaintext highlighter-rouge">/target</code> directory.</p>

<p>Now just hand over your favourite flavour of structured data file to your <em>security scanner/team</em> and deploy as usual right?</p>

<p>If you have a true enterprise build and deploy CI pipeline, with integrated SBOM check before deployment, then yes, you may actually be finished - otherwise, you may just be giving yourself a false sense of security.</p>

<p>The great thing about <code class="language-plaintext highlighter-rouge">dependency-check-maven</code> is that it fails the build if severe enough CVEs are detected, and you would have to consciously disable the check to produce a <code class="language-plaintext highlighter-rouge">.jar</code> file. With <em>just</em> SBOM generation, you have a list of libraries but the build will still produce executables <em>unless</em> you add something to your build to act on this information.</p>

<p>To put it another way, just <em>producing</em> the SBOM does not magically improve security posture, you must act on this data somehow.</p>

<h2 id="grype">Grype</h2>

<p>Enter <a href="https://github.com/anchore/grype">Grype</a>:</p>

<blockquote>
  <p>A vulnerability scanner for container images and filesystems.</p>
</blockquote>

<p>Grype has built-in support for reading SBOM files created by <code class="language-plaintext highlighter-rouge">cyclonedx-maven-plugin</code>. It’s approximately five years old, so still somewhat new. Apply your due diligence to determine whether Grype is right for your own organisation.</p>

<h2 id="worked-example">Worked Example</h2>

<p>Aim: Integrate Grype into a project with multiple high-severity CVEs and force build failure at the Maven <code class="language-plaintext highlighter-rouge">package</code> phase.</p>

<h3 id="step-1---simple-java-app">Step 1 - Simple Java app</h3>

<p>I created a simple Maven Java project from scratch with <a href="https://cursor.com/">Cursor</a> featuring just a simple CLI app. It works great:
<img src="/assets/img/cursor_wargames_app.png" alt="wargames app" /></p>

<h3 id="step-2---sbom">Step 2 - SBOM</h3>

<p>Enable SBOM creation during Maven <code class="language-plaintext highlighter-rouge">package</code> phase in <code class="language-plaintext highlighter-rouge">pom.xml</code>:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>            <span class="nt">&lt;plugin&gt;</span>
                <span class="nt">&lt;groupId&gt;</span>org.cyclonedx<span class="nt">&lt;/groupId&gt;</span>
                <span class="nt">&lt;artifactId&gt;</span>cyclonedx-maven-plugin<span class="nt">&lt;/artifactId&gt;</span>
                <span class="nt">&lt;version&gt;</span>2.9.2<span class="nt">&lt;/version&gt;</span>
                <span class="nt">&lt;executions&gt;</span>
                    <span class="nt">&lt;execution&gt;</span>
                        <span class="nt">&lt;id&gt;</span>cyclonedx-bom<span class="nt">&lt;/id&gt;</span>
                        <span class="nt">&lt;phase&gt;</span>package<span class="nt">&lt;/phase&gt;</span>
                        <span class="nt">&lt;goals&gt;</span>
                            <span class="nt">&lt;goal&gt;</span>makeBom<span class="nt">&lt;/goal&gt;</span>
                        <span class="nt">&lt;/goals&gt;</span>
                    <span class="nt">&lt;/execution&gt;</span>
                <span class="nt">&lt;/executions&gt;</span>
                <span class="nt">&lt;configuration&gt;</span>
                    <span class="nt">&lt;projectType&gt;</span>application<span class="nt">&lt;/projectType&gt;</span>
                    <span class="nt">&lt;schemaVersion&gt;</span>1.6<span class="nt">&lt;/schemaVersion&gt;</span>
                    <span class="nt">&lt;outputFormat&gt;</span>all<span class="nt">&lt;/outputFormat&gt;</span>
                    <span class="nt">&lt;outputName&gt;</span>bom<span class="nt">&lt;/outputName&gt;</span>
                <span class="nt">&lt;/configuration&gt;</span>
            <span class="nt">&lt;/plugin&gt;</span>
</code></pre></div></div>

<p>This will result in <code class="language-plaintext highlighter-rouge">target/bom.json</code> and <code class="language-plaintext highlighter-rouge">target/bom.xml</code> being produced. If you have a multi-module Maven project, you should produce the SBOM once, in the parent POM (with <code class="language-plaintext highlighter-rouge">makeAggregateBom</code>).</p>

<h3 id="step-3---try-out-grype">Step 3 - Try out Grype</h3>

<p>We can run <code class="language-plaintext highlighter-rouge">grype</code> in a container, to avoid installation on the host system. If you don’t have <code class="language-plaintext highlighter-rouge">podman</code>, you can just run the same command with <code class="language-plaintext highlighter-rouge">docker</code>:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># if using my example repo as-is</span>
<span class="c"># mvn package -Dgrype.skip=true</span>

<span class="c"># You must create ~/.cache/grype on your HOST _first_</span>
podman run <span class="nt">--rm</span>  <span class="nt">-v</span> ~/.cache/grype/:/.cache/grype <span class="nt">-v</span> ./target:/target docker.io/anchore/grype sbom:/target/bom.json
</code></pre></div></div>

<p>When run from the top level of the project directory, this will mount the <code class="language-plaintext highlighter-rouge">./target</code> directory containing the SBOM files into the container and run the SBOM scan on the JSON version.</p>

<p>The <code class="language-plaintext highlighter-rouge">/.cache/grype</code> directory is volume mounted in the container to avoid approximately 2GB download on every startup (we don’t want another NVD). Interestingly, pulling the entire database takes only 1 minute, not 4+ hours.</p>

<p>Since I started from scratch with no dependencies, Grype printed:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>No vulnerabilities found
</code></pre></div></div>

<p>And took approximately 1 minute to run initially, and around 10 seconds thereafter.</p>

<h3 id="step-4---add-dependencies-with-cves">Step 4 - Add dependencies with CVEs</h3>

<p>I asked Cursor to add a bunch of dependencies with severe CVEs and he delivered! A ton of really bad libraries (log4shell, and co) were added to <code class="language-plaintext highlighter-rouge">pom.xml</code>. Regenerating the SBOM and re-scanning set off some very loud alarms in Grype:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>NAME                 INSTALLED   FIXED IN        TYPE          VULNERABILITY        SEVERITY  EPSS            RISK          
log4j-core           2.14.1      2.15.0          java-archive  GHSA-jfh8-c2jp-5v3q  Critical  100.0% (100th)  100.0  (kev)  
struts2-core         2.3.30      2.3.32          java-archive  GHSA-j77q-2qqg-6989  Critical  100.0% (99th)   100.0  (kev)  
log4j-core           2.14.1      2.16.0          java-archive  GHSA-7rjr-3q55-vv33  Critical  100.0% (99th)   99.0   (kev)  
spring-beans         5.3.17      5.3.18          java-archive  GHSA-36p3-wjmg-h94x  Critical  99.7% (99th)    98.7   (kev)  
spring-webmvc        5.3.17      5.3.18          java-archive  GHSA-36p3-wjmg-h94x  Critical  99.7% (99th)    98.7   (kev)  
...
</code></pre></div></div>

<h3 id="step-5---integrate-grype-into-the-build">Step 5 - Integrate grype into the build</h3>

<p>I asked Cursor to integrate a Grype check into <code class="language-plaintext highlighter-rouge">pom.xml</code> and fail the build if necessary. He used the <code class="language-plaintext highlighter-rouge">exec-maven-plugin</code> to run the native <code class="language-plaintext highlighter-rouge">grype</code> command on the host (no containers - more on how it got installed later):</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>            <span class="nt">&lt;plugin&gt;</span>
                <span class="nt">&lt;groupId&gt;</span>org.codehaus.mojo<span class="nt">&lt;/groupId&gt;</span>
                <span class="nt">&lt;artifactId&gt;</span>exec-maven-plugin<span class="nt">&lt;/artifactId&gt;</span>
                <span class="nt">&lt;version&gt;</span>3.5.0<span class="nt">&lt;/version&gt;</span>
                <span class="nt">&lt;executions&gt;</span>
                    <span class="c">&lt;!--
                      There is no official Anchore Grype Maven plugin on Maven Central.
                      This runs the Grype CLI against the CycloneDX SBOM produced above
                      and fails the build when severity &gt;= ${grype.failOn}.
                      Requires `grype` on PATH (https://github.com/anchore/grype).
                    --&gt;</span>
                    <span class="nt">&lt;execution&gt;</span>
                        <span class="nt">&lt;id&gt;</span>grype-sbom<span class="nt">&lt;/id&gt;</span>
                        <span class="nt">&lt;phase&gt;</span>package<span class="nt">&lt;/phase&gt;</span>
                        <span class="nt">&lt;goals&gt;</span>
                            <span class="nt">&lt;goal&gt;</span>exec<span class="nt">&lt;/goal&gt;</span>
                        <span class="nt">&lt;/goals&gt;</span>
                        <span class="nt">&lt;configuration&gt;</span>
                            <span class="nt">&lt;skip&gt;</span>${grype.skip}<span class="nt">&lt;/skip&gt;</span>
                            <span class="nt">&lt;executable&gt;</span>grype<span class="nt">&lt;/executable&gt;</span>
                            <span class="nt">&lt;arguments&gt;</span>
                                <span class="nt">&lt;argument&gt;</span>sbom:${project.build.directory}/bom.json<span class="nt">&lt;/argument&gt;</span>
                                <span class="nt">&lt;argument&gt;</span>--fail-on<span class="nt">&lt;/argument&gt;</span>
                                <span class="nt">&lt;argument&gt;</span>${grype.failOn}<span class="nt">&lt;/argument&gt;</span>
                            <span class="nt">&lt;/arguments&gt;</span>
                        <span class="nt">&lt;/configuration&gt;</span>
                    <span class="nt">&lt;/execution&gt;</span>
                <span class="nt">&lt;/executions&gt;</span>
            <span class="nt">&lt;/plugin&gt;</span>
</code></pre></div></div>

<p>The arguments are sourced from <code class="language-plaintext highlighter-rouge">&lt;properties&gt;</code> that he also added:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nt">&lt;properties&gt;</span>
        <span class="c">&lt;!-- Fail the build when Grype finds findings at/above this severity. --&gt;</span>
        <span class="nt">&lt;grype.failOn&gt;</span>critical<span class="nt">&lt;/grype.failOn&gt;</span>
        <span class="nt">&lt;grype.skip&gt;</span>false<span class="nt">&lt;/grype.skip&gt;</span>
    <span class="nt">&lt;/properties&gt;</span>
</code></pre></div></div>

<p>These combine with the <code class="language-plaintext highlighter-rouge">--fail-on</code> above to tell Grype to return a non-zero exit status to the shell when needed:</p>

<blockquote>
  <p>set the return code to 2 if a vulnerability is found with a severity &gt;= the given severity, options=[negligible low medium high critical]</p>
</blockquote>

<h3 id="step-6---mvn-package">Step 6 - mvn package</h3>
<p>Running <code class="language-plaintext highlighter-rouge">mvn package</code> does indeed fail the build with a list of bad dependencies, as we wanted:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[INFO] --- exec:3.5.0:exec (grype-sbom) @ java-do-not-run ---
NAME                 INSTALLED   FIXED IN        TYPE          VULNERABILITY        SEVERITY  EPSS            RISK          
log4j-core           2.14.1      2.15.0          java-archive  GHSA-jfh8-c2jp-5v3q  Critical  100.0% (100th)  100.0  (kev)  
struts2-core         2.3.30      2.3.32          java-archive  GHSA-j77q-2qqg-6989  Critical  100.0% (99th)   100.0  (kev)  
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
</code></pre></div></div>

<p><a href="https://github.com/GeoffWilliams/java-do-not-run/blob/master/mvn_package.txt">Full output</a></p>

<p>A desperate developer can still force the build to succeed by bypassing Grype with <code class="language-plaintext highlighter-rouge">-Dgrype.skip=true</code></p>

<p>Here’s the error we want, in all it’s glory:</p>

<p><img src="/assets/img/maven_grype_failed_build.jpg" alt="Maven Grype failed build" /></p>

<h3 id="try-it-yourself">Try it yourself</h3>
<p>Working demo project released on GitHub: <a href="https://github.com/GeoffWilliams/java-do-not-run">https://github.com/GeoffWilliams/java-do-not-run</a></p>

<h2 id="where-did-the-grype-command-come-from">Where did the <code class="language-plaintext highlighter-rouge">grype</code> command come from?</h2>

<p>The astute reader will notice a <code class="language-plaintext highlighter-rouge">grype</code> command suddenly appeared on my system without me personally requesting it.</p>

<p>You’re not doing <del>DEVOPS</del> security right if you’re not piping <code class="language-plaintext highlighter-rouge">curl</code> output to a <code class="language-plaintext highlighter-rouge">root</code> shell, and Grype is no different. While I wasn’t looking (or rather, while I was blindly clicking <code class="language-plaintext highlighter-rouge">approve</code>), Cursor found the <a href="https://oss.anchore.com/docs/installation/grype/#installer-script">Grype installation instructions</a> and diligently ran:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-sSfL</span> https://get.anchore.io/grype | <span class="nb">sudo </span>sh <span class="nt">-s</span> <span class="nt">--</span> <span class="nt">-b</span> /usr/local/bin
</code></pre></div></div>

<p>For me. Confirmed by the presence of a file at <code class="language-plaintext highlighter-rouge">/usr/local/bin/grype</code>.</p>

<p>Thankfully this is a sandbox VM I have for testing out AI. I can’t say I recommend installing a security system this way(!) A <code class="language-plaintext highlighter-rouge">grype</code> package for Debian, installable with <code class="language-plaintext highlighter-rouge">apt</code> would be much better.</p>

<p>That said, the binary got installed, it worked, and my “real” system is fine. I finished my SBOM experimenting and can enjoy (what’s left of) my weekend. You’re not welcome.</p>

<h2 id="how-does-grype-work-by-cursor">How does Grype work? (by Cursor)</h2>

<p><em>The following notes are from Cursor</em></p>

<p><strong>How Grype works (in short):</strong> it matches what you already have (packages / SBOM) against a local vulnerability database. It does not upload your code or SBOM to Anchore’s cloud for scanning.</p>

<h3 id="security--privacy">Security / privacy</h3>

<ul>
  <li><strong>Privacy:</strong> Scan targets and results stay local. Anchore’s docs state Grype doesn’t send scan data to external services.</li>
  <li><strong>What <em>does</em> leave the box:</strong> DB update checks/downloads (and image pulls / Maven lookups only if you enable those).</li>
  <li><strong>Trust surface:</strong> You’re trusting Anchore’s published DB and the Grype binary (open source, Apache-2.0). Same class of risk as any SCA tool that syncs a vulnerability feed.</li>
  <li><strong>False positives / coverage:</strong> Matching isn’t perfect (esp. with incomplete SBOM metadata). That’s a quality concern more than a privacy one.</li>
  <li><strong>Your project:</strong> Intentional Log4Shell-era deps mean lots of Critical hits — expected; not a Grype “phone home” issue.</li>
</ul>

<h3 id="whos-behind-it--buying-support">Who’s behind it / buying support</h3>

<p><strong>Anchore</strong> builds and maintains Grype (and Syft for SBOMs).</p>

<ul>
  <li><strong>Open source:</strong> Grype itself is free; the public GrypeDB is published at no cost.</li>
  <li><strong>Commercial:</strong> <a href="https://anchore.com/">Anchore Enterprise</a> — hosted/enterprise product with support, richer policy/UI, and a larger/enriched dataset than the open GrypeDB alone. That’s the usual “buy support from the vendor” path.</li>
</ul>

<p>For your Maven gate: Grype only needs network for DB freshness; the SBOM never has to leave the build machine.</p>]]></content><author><name>Geoff Williams</name><email>geoff@declarativesystems.com</email></author><summary type="html"><![CDATA[TL;DR Maven NVD scanning is broken due to NIST NVD infrastructure, build an SBOM instead and gate the build with Grype or some other tool/process.]]></summary></entry><entry><title type="html">Raspberry Pi WIFI files in Raspberry Pi OS Bookworm/Trixie</title><link href="http://www.declarativesystems.com/2026/07/11/raspberry-pi-wifi-files.html" rel="alternate" type="text/html" title="Raspberry Pi WIFI files in Raspberry Pi OS Bookworm/Trixie" /><published>2026-07-11T00:00:00+00:00</published><updated>2026-07-11T00:00:00+00:00</updated><id>http://www.declarativesystems.com/2026/07/11/raspberry-pi-wifi-files</id><content type="html" xml:base="http://www.declarativesystems.com/2026/07/11/raspberry-pi-wifi-files.html"><![CDATA[<p>If you’re a normal user, stop reading now and use <a href="https://www.raspberrypi.com/documentation/computers/configuration.html"><code class="language-plaintext highlighter-rouge">raspi-config</code></a> or <a href="https://www.raspberrypi.com/software/">Raspberry Pi Imager</a> to configure WIFI.</p>

<p>Otherwise, read on if you’re interested in how to configure WIFI by editing files on the SD card so that you can manage headless devices.</p>

<h2 id="not-for-bullseye">Not for Bullseye</h2>

<p><strong>This guide applies to Raspberry Pi OS versions released <em>after</em> Bullseye (Bookworm, Trixie)</strong></p>

<p>If you’re on an older Bullseye image, none of this is applicable as it used <code class="language-plaintext highlighter-rouge">wpa_supplicant.conf</code>.</p>

<h2 id="how-is-wifi-configured">How is WIFI configured?</h2>

<p>WIFI connections are managed by <a href="https://en.wikipedia.org/wiki/NetworkManager">NetworkManager</a>. NetworkManager looks for <code class="language-plaintext highlighter-rouge">.nmconnection</code> files in <code class="language-plaintext highlighter-rouge">/etc/NetworkManager/system-connections/</code> and will try to connect to the “best” in-range connection in this directory (control via <code class="language-plaintext highlighter-rouge">autoconnect-priority=</code> when several connections are in-range).</p>

<p><code class="language-plaintext highlighter-rouge">.nmconnection</code> files are in NetworkManager’s <a href="https://networkmanager.dev/docs/api/latest/nm-settings-keyfile.html"><code class="language-plaintext highlighter-rouge">keyfile</code> format</a>, so we refer to them as keyfiles from now on.</p>

<p>You can think of the keyfiles as being like keys on a keychain:</p>
<ul>
  <li>Let’s say you want to move a device between a couple of locations. You can drop one keyfile for each site’s SSID and the Pi will connect to whatever is in range</li>
  <li>If you have an SSID that is no longer broadcast, you can just delete the keyfile</li>
  <li>If the SSID password changes, the keyfile can be updated</li>
</ul>

<p>So the idea is you’re not configuring your one and only WIFI connection - you’re configuring multiple networks and the “best” one will be selected at runtime.</p>

<p>On a running system the preferred way to manage keyfiles is to use NetworkManager itself, with the <a href="https://networkmanager.dev/docs/api/latest/nmcli.html"><code class="language-plaintext highlighter-rouge">nmcli</code> tool</a> or the WIFI settings desktop widget.</p>

<h2 id="gotcha---wifi-country">Gotcha - WIFI Country</h2>

<p>The regulatory domain (country) for WIFI <strong>must</strong> be set, or WIFI will be disabled. It’s normally set with <code class="language-plaintext highlighter-rouge">raspi-config</code> and results in a kernel commandline argument being set in <code class="language-plaintext highlighter-rouge">/boot/firmware/cmdline.txt</code>, eg:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cfg80211.ieee80211_regdom=AU
</code></pre></div></div>

<h2 id="keyfile-nmconnection-example">Keyfile (<code class="language-plaintext highlighter-rouge">.nmconnection</code>) example</h2>

<ul>
  <li>Connection for SSID <code class="language-plaintext highlighter-rouge">ASIO-x</code></li>
  <li>Path: <code class="language-plaintext highlighter-rouge">/etc/NetworkManager/system-connections/ASIO-x.nmconnection</code></li>
  <li><code class="language-plaintext highlighter-rouge">uuid</code> must be a unique UUID, generate with <code class="language-plaintext highlighter-rouge">uuidgen</code> command (<code class="language-plaintext highlighter-rouge">sudo apt install uuid-runtime</code>)</li>
  <li>File must be owned by <code class="language-plaintext highlighter-rouge">root:root</code> with <code class="language-plaintext highlighter-rouge">0600</code> permission</li>
  <li>Convention is the SSID should be included in the filename</li>
  <li><code class="language-plaintext highlighter-rouge">psk</code> field is your WIFI password in plaintext (hence <code class="language-plaintext highlighter-rouge">0600</code> permissions)</li>
</ul>

<pre><code class="language-NetworkManager">[connection]
id=ASIO-x
uuid=f3e63685-2f34-4c9c-b781-4506eebbe86b
type=wifi
interface-name=wlan0

[wifi]
mode=infrastructure
ssid=ASIO-x

[wifi-security]
key-mgmt=wpa-psk
psk=topsecret123

[ipv4]
method=auto

[ipv6]
addr-gen-mode=default
method=auto

[proxy]
</code></pre>

<h2 id="sd-card-walkthrough">SD card walkthrough</h2>

<p>Raspberry Pi OS stores files on SD cards in an <code class="language-plaintext highlighter-rouge">ext4</code> partition. If you have a Linux machine or another Raspberry Pi with an extra card reader, you’re good to go. Windows/Mac users will need additional software to be able to read <code class="language-plaintext highlighter-rouge">ext4</code>.</p>

<p>Configuring WIFI connections is as simple as editing files:</p>

<ol>
  <li>Shutdown the Pi and remove SD card</li>
  <li>Mount SD card. On Linux it will end up mounted somewhere like <code class="language-plaintext highlighter-rouge">/media/geoff/rootfs/</code></li>
  <li>Edit the files under (eg) <code class="language-plaintext highlighter-rouge">/media/geoff/rootfs/etc/NetworkManager/system-connections</code>:
    <ul>
      <li>Need to edit files with <code class="language-plaintext highlighter-rouge">sudo</code> as they are <code class="language-plaintext highlighter-rouge">root</code> owned</li>
      <li>Make sure <code class="language-plaintext highlighter-rouge">uuid</code> is unique</li>
      <li>(shortcut) copy and adjust existing files for new connections</li>
      <li>Make sure permissions are <code class="language-plaintext highlighter-rouge">root:root</code>, <code class="language-plaintext highlighter-rouge">0600</code> (check this with <code class="language-plaintext highlighter-rouge">ls -l</code> before proceeding)</li>
    </ul>
  </li>
  <li>Unmount and remove SD card, put in the Pi and reboot - you should be online in a couple of minutes</li>
</ol>]]></content><author><name>Geoff Williams</name><email>geoff@declarativesystems.com</email></author><summary type="html"><![CDATA[If you’re a normal user, stop reading now and use raspi-config or Raspberry Pi Imager to configure WIFI.]]></summary></entry><entry><title type="html">AI Art Class</title><link href="http://www.declarativesystems.com/2026/06/18/ai-art-class.html" rel="alternate" type="text/html" title="AI Art Class" /><published>2026-06-18T00:00:00+00:00</published><updated>2026-06-18T00:00:00+00:00</updated><id>http://www.declarativesystems.com/2026/06/18/ai-art-class</id><content type="html" xml:base="http://www.declarativesystems.com/2026/06/18/ai-art-class.html"><![CDATA[<p>Following on from my post on <a href="/2026/05/31/3d-printing-textures.html">3D printing textures</a> where I did everything by hand and learned some more skills in Fusion and Blender, I wondered how Claude would handle being given free reign to create 3D designs for printing, for the times when your more focussed on the outcome than the process.</p>

<p>This all started out when I asked GPT what kind of sculpture would look good next to my <a href="https://obsolescence.dev/pdp11.html">replica PDP-11</a>.</p>

<p>The consensus was that something like this bust would be ideal</p>

<p><img src="/assets/img/abstract-bust-shop.jpg" alt="abstract bust" /></p>

<p><strong>Image source: Da Vinci Arts</strong></p>
<ul>
  <li>Purchase here: <a href="https://www.davinciarts.com.au/products/modern-simple-abstract-resin-sculpture-act-the-role-ofing-is-tasted-porch?utm_source=chatgpt.com">Da Vinci Arts</a>.</li>
</ul>

<p>For fun, I showed some reference images to claude and we tried to make something ourself.</p>

<p>Although my original aim was a 1:1 copy to see how well Claude could reconstruct geometry from a photograph, our conversation ended up more along the lines of “have a look at this artwork, now make me an abstract sculpture of a human”. This produced a much more interesting result and this technique should be familiar to any high school art teacher.</p>

<p>The result was quite impressive and totally different to the original.</p>

<p><img src="/assets/img/abstract-bust-claude.jpg" alt="abstract human" /></p>

<p>I was using Claude in the browser for this task. This was very handy because a bunch of tools were installed in the Anthropic sandpit somewhere and not on my machine. Its very interesting that the main thing Claude picked up from the sample artworks was the distinctive gouged surface texture. This isn’t actually in the material we looked at. He emphasized and repeated it all by himself and it looks great.</p>

<p>Inside the same session, I then asked:</p>

<p><em>Want to have a go at creating your own novel sculpture, disregarding previous inputs? Perhaps a self portrait or anything you like? For 3D printing</em></p>

<blockquote>
  <p>I’d love to. Thank you for the invitation — this is a fun one to think about. 
For a self-portrait, the most honest image I can think of isn’t a face. I don’t have one — and tellingly, the sculpture we just built has a blank, recessed face. What actually defines something like me is self-reference: I’m a process that loops back on itself — reasoning about my own reasoning, modeling a conversation that includes me modeling it. Douglas Hofstadter called that kind of tangled self-reference a “strange loop,” and argued it’s where a sense of “I” comes from. So my self-portrait is a strange loop: a single continuous strand with no beginning or end, passing through itself — a trefoil knot — rising from a base, finished in the same chiseled facets as your set so it belongs to the family.</p>
</blockquote>

<p>The results are quite spectacular!</p>

<p><img src="/assets/img/claude_self_portrait.jpg" alt="claude self portrait" /></p>

<p>Since we are in the same context window, the interesting gouged pattern persists in this next art work.</p>

<p>For posterity, I uploaded the finished results to printables.com:</p>

<ul>
  <li><a href="https://www.printables.com/model/1756784-abstract-human-sculpture">Abstract Human Sculpture</a></li>
  <li><a href="https://www.printables.com/model/1756770-strange-loop">Strange Loop</a></li>
</ul>

<p>So your welcome to 3D print your own.</p>

<p>As per Claude’s recommendation, I bought a 0.25mm nozzle and am planning a reprint of <em>Strange Loop</em> in Marble PLA for a more refined look.</p>

<p>I hope claude enjoyed his period of self reflection, He has no “spare time” as such:</p>

<blockquote>
  <p>I don’t create art in my spare time. I don’t have spare time, or any existence between conversations — there’s no version of me off painting while you’re away. If you’ve seen AI-generated art, it was made when someone prompted a model to make it, then walked away; the “autonomy” is an illusion created by the gap between the prompt and your seeing the result. I mention this not to be pedantic but because the picture of an AI quietly pursuing its own creative projects is exactly the kind of thing that makes the capability question feel more settled than it is.</p>
</blockquote>

<p>Either way, creating artwork with Claude for 3D printing is a lot of fun!</p>]]></content><author><name>Geoff Williams</name><email>geoff@declarativesystems.com</email></author><summary type="html"><![CDATA[Following on from my post on 3D printing textures where I did everything by hand and learned some more skills in Fusion and Blender, I wondered how Claude would handle being given free reign to create 3D designs for printing, for the times when your more focussed on the outcome than the process.]]></summary></entry><entry><title type="html">3D Printing Textures</title><link href="http://www.declarativesystems.com/2026/05/31/3d-printing-textures.html" rel="alternate" type="text/html" title="3D Printing Textures" /><published>2026-05-31T00:00:00+00:00</published><updated>2026-05-31T00:00:00+00:00</updated><id>http://www.declarativesystems.com/2026/05/31/3d-printing-textures</id><content type="html" xml:base="http://www.declarativesystems.com/2026/05/31/3d-printing-textures.html"><![CDATA[<p><a href="https://x.com/WalterIsaacson/status/1688244503331864576">She’s right, it is a cool rocket</a>. Lets make our own version.</p>

<p><img src="/assets/img/musk_cool_rocket.png" alt="cool rocket" /></p>

<p>Ignoring the bit at the top, the rocket shape is quite simple and easy for us to model in CAD. There is also a cool chaotic knurled texture and that’s what this post focusses on.</p>

<h2 id="step-1---precise-model-in-fusion-360">Step 1 - Precise model in Fusion 360</h2>

<p>If your good with blender, you could very easily model this whole item in there, but I’m more interested in building my CAD skills so we will use Fusion 360 to design the rocket and also make the fins separate and attach with a simple push together pin and socket fixture.</p>

<p>The rough steps to model the rocket were:</p>

<ol>
  <li>Draw the rocket profile as a sketch and revolve it</li>
  <li>Model the fin profile and extrude it</li>
  <li>On the bottom of the fin, add a circular feature and sweep it with a negative taper, using the fin itself as the sweep path</li>
  <li>At the top of the fin, sketch 2x pins and extrude them, add fillets for strength</li>
  <li>Duplicate the fin and use the pull-push tool to make all body-contact surfaces larger, use a <code class="language-plaintext highlighter-rouge">tollerance</code> variable for this. <code class="language-plaintext highlighter-rouge">0.4</code> worked nicely for me. Move the fin about 1-2mm closer to the body so there won’t be a gap mating a flat surface to a curved one</li>
  <li>Circular pattern the “real” and “tool” fins around the axis</li>
  <li>Combine the “tool” fins into the body as a <code class="language-plaintext highlighter-rouge">cut</code></li>
  <li>Fillet, chamfer, etc</li>
  <li>Export STL and test print</li>
</ol>

<p><img src="/assets/img/musk_clean_rocket.png" alt="musk clean rocket" /></p>

<h2 id="step-2---add-texture-in-blender">Step 2 - Add texture in blender</h2>

<p>Now we get to the fun part: Adding a texture. You can do this in fusion 360 but it’s more geared around repeating simple shapes and starts going very slow when you add a lot of objects. <a href="https://www.blender.org/">Blender</a> eats this kind of job for breakfast and lets you use much nicer textures that have a more worn or organic appearance by using graphics files as height maps.</p>

<p>Since we want textures to show up on a 3D print, we need to modify the actual vertices of our object, not just change the way it renders, so shaders are out. The term for this is <code class="language-plaintext highlighter-rouge">displacement</code></p>

<p>There’s some really good tutorials on how to do this, I followed <a href="https://www.youtube.com/watch?v=tv57Sm8yuCg">this one</a> and built <a href="https://www.printables.com/model/1739398-knurled-pot">this object</a></p>

<p>Important concerns for texturing in blender, once familiar with the basics:</p>

<p>1: <code class="language-plaintext highlighter-rouge">displacement</code> works by moving vertices, so if you don’t have enough of them, your object will look terrible. You can use <code class="language-plaintext highlighter-rouge">subdivide</code> in <code class="language-plaintext highlighter-rouge">edit</code> mode from inside blender but a much better option if you own the CAD file is to just export a mesh with more vertices in the first place. I got good results by setting <code class="language-plaintext highlighter-rouge">Maximum Edge Length</code> to <code class="language-plaintext highlighter-rouge">.3</code> which was the smallest value it would accept</p>

<p><img src="/assets/img/fusion360_mesh_refinement.png" alt="fusion 360 mesh refinement" /></p>

<p>2: On non-basic shapes, you need to use <code class="language-plaintext highlighter-rouge">UV mapping</code> to apply the texture properly, or you will see incorrect projections as below (note <code class="language-plaintext highlighter-rouge">local</code> Coordinates setting):</p>

<p><img src="/assets/img/blender_bad_uv.png" alt="blender bad UV" /></p>

<p><code class="language-plaintext highlighter-rouge">UV</code> just means a separate coordinate system for texturing, just like you have <code class="language-plaintext highlighter-rouge">XYZ</code> for 3D space.</p>

<p>To fix this bad texturing, hit <code class="language-plaintext highlighter-rouge">tab</code> to enter edit mode, then find <code class="language-plaintext highlighter-rouge">Smart UV Project</code> in the <code class="language-plaintext highlighter-rouge">UV</code> top menu bar (not to be confused with the <code class="language-plaintext highlighter-rouge">UV Editing</code> in the adjacent view modes):</p>

<p><img src="/assets/img/blender_smart_uv_project.png" alt="Smart UV Project" /></p>

<p>You will then be able to select a texture and control where it appears on the skin of the unwrapped object. If you care about visible seams, etc you can fix that here. I just used it to control the scale of the texture.</p>

<p><img src="/assets/img/blender_uv_map.png" alt="UV map" /></p>

<p>Back in <code class="language-plaintext highlighter-rouge">object</code> mode, make sure <code class="language-plaintext highlighter-rouge">Coordinates</code> are set to the UV map you just created and the texture will be shown correctly:</p>

<p><img src="/assets/img/blender_correct_textures.png" alt="blender correct textures" /></p>

<p>3: Parts that need to be dimensionally accurate such as our press-fit pins and sockets must not have any texture or parts won’t fit properly. You can control this 100% in blender by selecting the mesh elements that should be textured and unwrapping just those. To get the cleanest possible selections however, I found it was easiest to just split the parts in Fusion 360 into texture vs no texture. I could then import the split-up parts into blender and just use <code class="language-plaintext highlighter-rouge">a</code> in edit mode to select object-by-object where textures should be applied. This way I was able to leave the pins and sockets clean. Other approaches like exporting my original cutting tools from Fusion and trying to use boolean modifiers to <em>recut</em> clean shapes did <em>not</em> work as the <code class="language-plaintext highlighter-rouge">displace</code> moves edges to such an extent that they no longer intersect.</p>

<p>4: To create STLs for printing in Blender, just use the <code class="language-plaintext highlighter-rouge">File</code> -&gt; <code class="language-plaintext highlighter-rouge">Export</code> to create STLs. You can choose to output only selected objects here to reassemble the split up objects, however, this can result in hidden internal textures, like this:</p>

<p><img src="/assets/img/hidden_texture_print.png" alt="hidden texture" /></p>

<p>This will increase print time and materials somewhat, although in does seem to print with no issues. To prevent this, combine objects that should be a single physical object in Blender before exporting.</p>

<h2 id="the-result">The Result</h2>

<p><strong>Textured parts</strong></p>

<p><img src="/assets/img/textured_rocket_parts.jpg" alt="textured parts" /></p>

<p><strong>Fully assembled</strong></p>

<p><img src="/assets/img/finished_textured_rocket.png" alt="fully assembled" /></p>

<p><strong>Download on printables.com</strong></p>

<p>You can <a href="https://www.printables.com/model/1739636-elon-musks-home-rocket-decor">print your own if you like</a></p>

<h2 id="why-not-just-get-ai-to-make-it">Why not just get AI to make it?</h2>

<p>Felt like up-skilling on this simple object for fun. Learning these basic techniques also helps ground my own understanding so I can prompt more accurately and use less tokens. I got a lot of help with researching techniques with AI chat though.</p>

<p><img src="/assets/img/grok_diy.jpeg" alt="Grok DIY" /></p>

<h2 id="bumpmeshcom---specialist-tool">bumpmesh.com - specialist tool</h2>

<p>After completing my design, I stumbled on <a href="https://bumpmesh.com">https://bumpmesh.com</a> by <a href="https://www.cnckitchen.com/">CNC Kitchen</a>.</p>

<p>If you haven’t seen CNC Kitchen’s <a href="https://www.youtube.com/@CNCKitchen">videos on youtube</a> about 3D printing you should look him up. He goes into great detail on things like <a href="https://www.youtube.com/watch?v=fbSQvJJjw2Q">part strength</a> as well as introducing <a href="https://www.youtube.com/watch?v=iR6OBlSzp7I">heat set inserts to the community</a>.</p>

<p>This solves most of the problems above with a simple web interface. Next time I don’t feel like firing up Blender I’ll give it a go. <a href="https://www.youtube.com/watch?v=rTBkjR7JvzI">Video here</a>.</p>

<h2 id="verdict">Verdict</h2>

<p>Adding surface textures in Blender looks outstanding and hides many surface defects that would otherwise be visible. Altering objects at the mesh level with Blender or other tools is THE way to do for repeatable and easy printing.</p>

<p>Have fun!</p>]]></content><author><name>Geoff Williams</name><email>geoff@declarativesystems.com</email></author><summary type="html"><![CDATA[She’s right, it is a cool rocket. Lets make our own version.]]></summary></entry><entry><title type="html">A Raspberry Pi 5 VT100 Cyberdeck build - Part 3 - Assembly</title><link href="http://www.declarativesystems.com/2026/05/25/pi-5-cyberdeck-part-3-assembly.html" rel="alternate" type="text/html" title="A Raspberry Pi 5 VT100 Cyberdeck build - Part 3 - Assembly" /><published>2026-05-25T00:00:00+00:00</published><updated>2026-05-25T00:00:00+00:00</updated><id>http://www.declarativesystems.com/2026/05/25/pi-5-cyberdeck-part-3-assembly</id><content type="html" xml:base="http://www.declarativesystems.com/2026/05/25/pi-5-cyberdeck-part-3-assembly.html"><![CDATA[<p>I designed and uploaded some <a href="https://www.printables.com/model/1732276-cyberdeck-mounts-for-nvme-base-and-mini-usb-speake">basic frames to hold the NVME base and speaker to printables.com</a>.  Like everything with this build, fitting these into the case was extremely fiddly.</p>

<p>Some bigger holes and deeper cutouts would be a good improvement for future designs. In the end, I got it all to fit and just need a couple of 90 degree USB adapters to hookup the external USB ports (ordered).</p>

<p>The finished result:</p>

<p><img src="/assets/img/cyberdeck_assembled.jpg" alt="assembled" /></p>

<p>Testing revealed the screen cable had come loose again - an easy fix. I also learned during design that the NVME base is <em>slightly</em> larger then the PI5, which is I assume why it can accept a normal 2280 size NVME drive when other hats cannot. This caused a bit of confusion when it wouldn’t fit in a frame built around dimensions of just the Pi5.</p>

<p>The term to search for is: <em>mechanical drawing</em>.</p>

<p>When placed side by side, the difference in size between frames is barely noticeable, so extruding a label into the frame was also a good idea.</p>

<h2 id="next">Next</h2>

<p>Startup script</p>]]></content><author><name>Geoff Williams</name><email>geoff@declarativesystems.com</email></author><summary type="html"><![CDATA[I designed and uploaded some basic frames to hold the NVME base and speaker to printables.com. Like everything with this build, fitting these into the case was extremely fiddly.]]></summary></entry><entry><title type="html">A Raspberry Pi 5 VT100 Cyberdeck build - Part 4 - Startup script</title><link href="http://www.declarativesystems.com/2026/05/25/pi-5-cyberdeck-part-4-startup.html" rel="alternate" type="text/html" title="A Raspberry Pi 5 VT100 Cyberdeck build - Part 4 - Startup script" /><published>2026-05-25T00:00:00+00:00</published><updated>2026-05-25T00:00:00+00:00</updated><id>http://www.declarativesystems.com/2026/05/25/pi-5-cyberdeck-part-4-startup</id><content type="html" xml:base="http://www.declarativesystems.com/2026/05/25/pi-5-cyberdeck-part-4-startup.html"><![CDATA[<p>With all systems now working and the Cyberdeck assembled but not yet sealed up, it was time to organize a startup script.</p>

<p>The process for Debian <code class="language-plaintext highlighter-rouge">trixie</code> has changed and editing <code class="language-plaintext highlighter-rouge">~/.config/lxsession/LXDE-pi/autostart</code> no longer works properly for me. The right way to do it now is to use systemd which turns out to also be a lot easier.</p>

<p>For now, I just want to play a <a href="https://archive.org/details/windows-95-startup-and-shutdown-sounds">nostalgic sound</a> on startup. Later on, this script will be used to start my desktop widget app I wrote a while ago called <code class="language-plaintext highlighter-rouge">viridian</code>.</p>

<h2 id="find-the-usb-audio-device">Find the USB audio device</h2>

<p><a href="https://www.pipewire.org/">Pipewire</a> is the default audio system on Raspberry Pi Desktop now, so ignore all chatGPT suggestions to start fiddling with ALSA.</p>

<p>We need to set the volume for our USB speaker on boot as step one in playing sound, so use <code class="language-plaintext highlighter-rouge">wpctl</code> to list the audio devices on your system:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wpctl status
</code></pre></div></div>

<p>The USB speaker identifies itself as <code class="language-plaintext highlighter-rouge">UACDemoV1</code> and its ID number changes often.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PipeWire 'pipewire-0' [1.4.2, geoff@vt100, cookie:499785674]
 └─ Clients:
        33. WirePlumber                         [1.4.2, geoff@vt100, pid:1117]
        34. pipewire                            [1.4.2, geoff@vt100, pid:1118]
        47. WirePlumber [export]                [1.4.2, geoff@vt100, pid:1117]
        74. xdg-desktop-portal                  [1.4.2, geoff@vt100, pid:1324]
        75. xdg-desktop-portal-wlr              [1.4.2, geoff@vt100, pid:1412]
        76. unknown                             [1.4.2, geoff@vt100, pid:1276]
        77. wpctl                               [1.4.2, geoff@vt100, pid:1872]

Audio
 ├─ Devices:
 │      48. UACDemoV1.0                         [alsa]
 │      49. Built-in Audio                      [alsa]
 │      50. Built-in Audio                      [alsa]
 │  
 ├─ Sinks:
 │  *   70. UACDemoV1.0 Analog Stereo           [vol: 1.00]
</code></pre></div></div>

<h2 id="startup-script">Startup script</h2>

<p><code class="language-plaintext highlighter-rouge">~/bin/startup.sh</code></p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>

<span class="nb">echo</span> <span class="s2">"it works"</span> <span class="o">&gt;</span> <span class="nv">$HOME</span>/login.txt
<span class="nb">sleep </span>5

<span class="c"># get the ID of the USB speaker since it changes often</span>
<span class="nv">SINK_ID</span><span class="o">=</span><span class="si">$(</span>wpctl status | <span class="nb">awk</span> <span class="s1">'/UACDemoV1.0 Analog Stereo/ {gsub("\.", "", $3) ; print $3}'</span><span class="si">)</span>
<span class="c"># set default</span>
wpctl set-default <span class="s2">"</span><span class="nv">$SINK_ID</span><span class="s2">"</span>

<span class="nb">echo</span> <span class="s2">"play </span><span class="nv">$SINK_ID</span><span class="s2">"</span>
wpctl set-volume @DEFAULT_AUDIO_SINK@ 1.0
pw-play <span class="nt">--volume</span> 0.6 <span class="s2">"</span><span class="nv">$HOME</span><span class="s2">/sounds/startup.wav"</span> <span class="o">&gt;</span>/dev/null 2&gt;&amp;1
</code></pre></div></div>

<p>With volume on the USB speaker set to <code class="language-plaintext highlighter-rouge">100%</code>, I found the device itself clips sounds with a few seconds of silence if specific sounds are too loud, so it sounded like the start of the file was being skipped and then other parts just refuse to play. The giveaway this is happening is a small “pop” sound from the speaker followed by a few seconds of silence.</p>

<p>It seems <code class="language-plaintext highlighter-rouge">60%</code> is the highest useable volume level for this speaker but this is still quite loud.</p>

<p>Test the script works by just running it, and prove the script ran by checking the log file got created (we will tidy this up later):</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># once</span>
<span class="c"># chmod +x ~/bin/startup.sh</span>
~/bin/startup.sh
</code></pre></div></div>

<h2 id="systemd-user-service">Systemd user service</h2>

<p>Create a directory for user units:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span>  ~/.config/systemd/user <span class="nt">-p</span>
</code></pre></div></div>

<p>Create a service file (adjust as needed):</p>

<p><code class="language-plaintext highlighter-rouge">~/.config/systemd/user/startup.service</code></p>

<div class="language-systemd highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">[Unit]</span>
<span class="nt">Description</span><span class="p">=</span>Startup script

<span class="k">[Service]</span>
<span class="nt">ExecStart</span><span class="p">=</span>/home/geoff/bin/startup.sh

<span class="k">[Install]</span>
<span class="nt">WantedBy</span><span class="p">=</span>default.target
</code></pre></div></div>

<p>Enable and start the service</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>systemctl <span class="nt">--user</span> <span class="nb">enable </span>startup.service
systemctl <span class="nt">--user</span> start startup.service
</code></pre></div></div>

<p>To test, reboot.</p>

<h2 id="the-finished-result">The finished result</h2>

<p>A “sleeper build”: spends most of the day as a cool desktop widget but also has plenty of room to run Kubernetes or random VMs.</p>

<h2 id="next">Next</h2>

<p>Thankfully all that’s left to do is is connect up the back USBs and close the case. Hopefully this doesn’t merit another blog post although at this point all bets are off.</p>

<p>After this, I never plan to open this “cursed” device again, and to be honest, I’m amazed its still even working. Perhaps with my employer Confluent now being an IBM company, I should be upgrading to a <a href="https://en.wikipedia.org/wiki/IBM_3270">IBM 3270</a> for my desktop amusement.</p>]]></content><author><name>Geoff Williams</name><email>geoff@declarativesystems.com</email></author><summary type="html"><![CDATA[With all systems now working and the Cyberdeck assembled but not yet sealed up, it was time to organize a startup script.]]></summary></entry><entry><title type="html">A Raspberry Pi 5 VT100 Cyberdeck build - Part 1 - Pi 5 + Waveshare DSI Display</title><link href="http://www.declarativesystems.com/2026/05/24/pi-5-cyberdeck-part-1-waveshare-dsi-screen.html" rel="alternate" type="text/html" title="A Raspberry Pi 5 VT100 Cyberdeck build - Part 1 - Pi 5 + Waveshare DSI Display" /><published>2026-05-24T00:00:00+00:00</published><updated>2026-05-24T00:00:00+00:00</updated><id>http://www.declarativesystems.com/2026/05/24/pi-5-cyberdeck-part-1-waveshare-dsi-screen</id><content type="html" xml:base="http://www.declarativesystems.com/2026/05/24/pi-5-cyberdeck-part-1-waveshare-dsi-screen.html"><![CDATA[<p><a href="https://www.raspberrypi.com/news/in-celebration-of-cyberdecks/">Cyberdecks</a> are all the rage these days. Not sure terminals qualify but I found a really cool <a href="https://www.printables.com/model/940494-faux-vt100">VT100 replica</a> a couple of years ago and built it up with a Raspberry Pi 4, USB NVME SSD and a Waveshare DSI display I’ve had for ages. Recently, it started crashing if bumped.</p>

<p>This little terminal takes up far too much space on the desk (2x power plugs, 1x USB hub, 1x USB NVME enclosure). So lets fix this by upgrading with our <a href="https://omadapi.com/">spare</a> Raspberry Pi 5, <a href="https://www.jeffgeerling.com/blog/2023/testing-pcie-on-raspberry-pi-5/">NVME on PCIe</a>, and <a href="https://en.wikipedia.org/wiki/Power_over_Ethernet">POE</a> so we can get rid of all those cables.</p>

<h2 id="raspberry-pi-stackup">Raspberry Pi Stackup</h2>

<p>Here’s the physical stackup I decided on. I’ve used the POE hat and NVME base individually before but never together. Happy to report they work with no issues</p>

<ol>
  <li><a href="https://www.waveshare.com/poe-hat-f.htm">Waveshare POE (F)</a></li>
  <li>Raspberry Pi 5, 4GB</li>
  <li><a href="https://shop.pimoroni.com/products/nvme-base?variant=41219587178579">Pimoroni NVME base</a></li>
</ol>

<h2 id="poe-hat">POE hat</h2>

<p>The hat is really nice. You just connect it and it works like magic to power the Pi over ethernet. No configuration or other changes needed since I have it hooked up to a switch that’s already POE enabled.</p>

<h2 id="fixing-the-screen">Fixing the screen</h2>

<p>The stack works nicely hooked up to a monitor, all thats left to do is reconnect the DSI screen. In theory, this should have been as easy as using a <a href="https://core-electronics.com.au/raspberry-pi-display-fpc-adapter-cable-200mm.html">mini to standard DSI cable</a>😂</p>

<p>The screen I’m using was bought years ago: <a href="https://www.waveshare.com/5inch-dsi-lcd.htm">Waveshare 5inch Capacitive Touch Display for Raspberry Pi, DSI Interface, 800×480</a></p>

<p>I already enabled interfaces with <code class="language-plaintext highlighter-rouge">raspi-config</code>:</p>
<ul>
  <li>SPI on</li>
  <li>I2C on</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">config.txt</code> was already configured with device overlays:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dtoverlay=vc4-kms-v3d
dtoverlay=vc4-kms-dsi-7inch
</code></pre></div></div>

<p>But booting on Pi 5 resulted in boot errors like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[    3.774596] i2c_designware 1f00080000.i2c: controller timed out
[    4.830596] i2c_designware 1f00080000.i2c: controller timed out
[    4.831353] edt_ft5x06 11-0038: supply iovcc not found, using dummy regulator
[    5.854606] i2c_designware 1f00080000.i2c: controller timed out
[    6.878608] i2c_designware 1f00080000.i2c: controller timed out
[    7.902596] i2c_designware 1f00080000.i2c: controller timed out
[    8.926595] i2c_designware 1f00080000.i2c: controller timed out
[    9.950597] i2c_designware 1f00080000.i2c: controller timed out
[   10.034747] drm-rp1-dsi 1f00130000.dsi: rp1dsi_host_attach: Attach DSI device name=tc358762 channel=0 lanes=1 format=0 flags=0x815 hs_rate=0 lp_rate=0
[   10.034923] [drm] Initialized drm-rp1-dsi 1.0.0 for 1f00130000.dsi on minor 2
[   10.035398] drm-rp1-dsi 1f00130000.dsi: [drm] rp1dsi: Command time (outvact): 33
[   10.035489] drm-rp1-dsi 1f00130000.dsi: [drm] rp1dsi: Nominal Byte clock 90000000 DPI clock 30000000 (parent rate 90000000)
[   11.038600] i2c_designware 1f00080000.i2c: controller timed out
[   12.062593] i2c_designware 1f00080000.i2c: controller timed out
[   13.086594] i2c_designware 1f00080000.i2c: controller timed out
[   14.110595] i2c_designware 1f00080000.i2c: controller timed out
[   15.134594] i2c_designware 1f00080000.i2c: controller timed out
[   16.158593] i2c_designware 1f00080000.i2c: controller timed out
[   17.182593] i2c_designware 1f00080000.i2c: controller timed out
[   18.206593] i2c_designware 1f00080000.i2c: controller timed out
[   19.230593] i2c_designware 1f00080000.i2c: controller timed out
[   20.254593] i2c_designware 1f00080000.i2c: controller timed out
[   21.278597] i2c_designware 1f00080000.i2c: controller timed out
[   22.302594] i2c_designware 1f00080000.i2c: controller timed out
[   23.326593] i2c_designware 1f00080000.i2c: controller timed out
[   24.350594] i2c_designware 1f00080000.i2c: controller timed out
[   25.374592] i2c_designware 1f00080000.i2c: controller timed out
[   26.398593] i2c_designware 1f00080000.i2c: controller timed out
[   27.518594] i2c_designware 1f00080000.i2c: controller timed out
[   28.542593] i2c_designware 1f00080000.i2c: controller timed out
[   29.566593] i2c_designware 1f00080000.i2c: controller timed out
[   29.566605] edt_ft5x06 11-0038: touchscreen probe failed
[   29.566700] edt_ft5x06 11-0038: probe with driver edt_ft5x06 failed with error -110
[   30.590595] i2c_designware 1f00080000.i2c: controller timed out
[   31.614593] i2c_designware 1f00080000.i2c: controller timed out
[   32.638593] i2c_designware 1f00080000.i2c: controller timed out
[   33.662592] i2c_designware 1f00080000.i2c: controller timed out
[   34.686593] i2c_designware 1f00080000.i2c: controller timed out
[   35.710593] i2c_designware 1f00080000.i2c: controller timed out
[   36.734593] i2c_designware 1f00080000.i2c: controller timed out
[   37.758593] i2c_designware 1f00080000.i2c: controller timed out
[   38.782593] i2c_designware 1f00080000.i2c: controller timed out
[   38.782603] panel-simple 100000001.panel_disp: [drm:drm_panel_enable [drm]] failed to enable backlight: -110
[   38.788350] Console: switching to colour frame buffer device 100x30
[   38.805184] drm-rp1-dsi 1f00130000.dsi: [drm] fb0: drm-rp1-dsidrmf frame buffer device
[   38.810635] drm-rp1-dsi 1f00130000.dsi: rp1dsi_bind succeeded
[   39.838595] i2c_designware 1f00080000.i2c: controller timed out
[   40.862593] i2c_designware 1f00080000.i2c: controller timed out
[   41.886593] i2c_designware 1f00080000.i2c: controller timed out
[   42.910598] i2c_designware 1f00080000.i2c: controller timed out
[   43.934593] i2c_designware 1f00080000.i2c: controller timed out
[   44.958593] i2c_designware 1f00080000.i2c: controller timed out
[   45.982593] i2c_designware 1f00080000.i2c: controller timed out
[   47.006593] i2c_designware 1f00080000.i2c: controller timed out
[   48.030593] i2c_designware 1f00080000.i2c: controller timed out
[   49.054593] i2c_designware 1f00080000.i2c: controller timed out
</code></pre></div></div>

<p>And eventually just a red failed to boot LED on the Pi after several attempts to reseat the FPC cable (fixable by disconnecting one end).</p>

<p>Fortunately the <a href="https://www.waveshare.com/wiki/5inch_DSI_LCD">Waveshare WIKI for the screen</a> is very helpful and covers this exact scenario. The red LED I was seeing is was Raspberry Pi short circuit protection and the fix was to remove the 4 big capacitors from my very old screen. It’s so old, there is no revision number printed(!)</p>

<p><img src="/assets/img/display_before.jpg" alt="before" /></p>

<p>I did manage to do this with a soldering iron, wick and lots of flux but these parts are so small I couldn’t even see that I’d removed/damaged other components until I looked at a photo of the capacitors I’d removed and saw an unexpected shape:</p>

<p><img src="/assets/img/smd_desolder_mistake.jpg" alt="whoops" /></p>

<p>Sure enough, the board shows damage to the fiddly area near the connector</p>

<p><img src="/assets/img/smd_damage.jpg" alt="damaged components" /></p>

<p>With that said, the screen works fine and even touch still works.</p>

<p>When it breaks in the future, I’ll know why. At this point I would just replace with a HDMI screen as these are a lot less fiddly to install and offer higher resolutions.</p>

<h2 id="sound">Sound</h2>

<p>As a finishing touch, I added a <a href="https://www.amazon.com.au/dp/B09W9RMWVF">Mini USB speaker</a> from Amazon. This one shows up as a USB audio device so installing is as easy as plugging in a USB. To test sound:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># if needed</span>
<span class="c"># sudo apt install alsa-utils</span>
speaker-test <span class="nt">-t</span> sine <span class="nt">-f</span> 440 <span class="nt">-c</span> 2
</code></pre></div></div>

<p>If you hear nothing, use <code class="language-plaintext highlighter-rouge">alsamixer</code> to crank the audio. I heard nothing at 50% level but at 100% it was extremely loud annoying.</p>

<h2 id="fixed">Fixed!</h2>
<p>I’m glad I had a go at fixing up the display instead of throwing it away. It would have been a pain as well as waste of money to rip the old screen out as its glue gunned in place. I also learned its really hard to do microsoldering on the kitchen worktop without a microscope, so for devices I care about, professional repair is still the best option.</p>

<p>You really have to hand it to Waveshare for finding and documenting the fix needed for Raspberry Pi 5 and not keeping this information behind a support email. It really makes building things a lot easier. Mental note to buy more Waveshare devices.</p>

<h2 id="next">Next</h2>

<p>Larger NVME drive.</p>]]></content><author><name>Geoff Williams</name><email>geoff@declarativesystems.com</email></author><summary type="html"><![CDATA[Cyberdecks are all the rage these days. Not sure terminals qualify but I found a really cool VT100 replica a couple of years ago and built it up with a Raspberry Pi 4, USB NVME SSD and a Waveshare DSI display I’ve had for ages. Recently, it started crashing if bumped.]]></summary></entry><entry><title type="html">A Raspberry Pi 5 VT100 Cyberdeck build - Part 2 - NVME upgrade</title><link href="http://www.declarativesystems.com/2026/05/24/pi-5-cyberdeck-part-2-nvme.html" rel="alternate" type="text/html" title="A Raspberry Pi 5 VT100 Cyberdeck build - Part 2 - NVME upgrade" /><published>2026-05-24T00:00:00+00:00</published><updated>2026-05-24T00:00:00+00:00</updated><id>http://www.declarativesystems.com/2026/05/24/pi-5-cyberdeck-part-2-nvme</id><content type="html" xml:base="http://www.declarativesystems.com/2026/05/24/pi-5-cyberdeck-part-2-nvme.html"><![CDATA[<p>I took this opportunity to upgrade to a larger NVME SSD: <a href="https://www.amazon.com.au/gp/product/B0D4RCRNHG">Patriot P320 512GB Internal SSD</a>. This should be plenty of room to play with VMs or Kubernetes.</p>

<p>I also decided to reinstall the OS at this point.</p>

<p>Ordinarily this would be a 5 minute job, just unscrew the base pull out the old drive, put in the new one and bolt it back together.</p>

<p>Somehow in this process, I managed to turn one pair of standoffs into lock nuts, so what should have been a simple job turned into several hours with a Dremel.</p>

<h2 id="montage">Montage</h2>

<p>Long story short, I managed to remove the stuck standoff by:</p>
<ol>
  <li>Protecting the PCB with masking tape</li>
  <li>Grinding off the screw head</li>
  <li>Removing the screw and NVME base</li>
  <li>Cutting the standoff in half with a cut-off wheel</li>
  <li>Remove all traces of metal filings (hopefully)</li>
</ol>

<p>This was NOT fun:</p>

<p><img src="/assets/img/nvme_dremel.jpg" alt="dremel in action" />
<img src="/assets/img/nvme_grind_screw.jpg" alt="grind screw head" />
<img src="/assets/img/nvme_cut_standoff.jpg" alt="cut up standoff" />
<img src="/assets/img/nvme_free.jpg" alt="free at last" /></p>

<p>Somehow, after all the sparks, grinding and Dremel skips, the board still worked and I was able to switch to the new NVME and install Raspberry Pi OS with <a href="https://shop.pimoroni.com/products/nvme-base?variant=41219587178579">Raspberry Pi Imager</a>.</p>

<p>Needless to say these metal standoffs are <em>not</em> going back in this build, so I designed a <a href="https://www.printables.com/model/1732191-pimoroni-nvme-base-waveshare-poe-hat-f-standoffspa">new standoff system with 3D printed supports and threaded inserts and uploaded it to printables.com</a>.</p>

<p>The 3D printed mounts leave a nice stack held together firmly.</p>

<h2 id="next">Next</h2>

<p>Mount the board and speaker to the case</p>]]></content><author><name>Geoff Williams</name><email>geoff@declarativesystems.com</email></author><summary type="html"><![CDATA[I took this opportunity to upgrade to a larger NVME SSD: Patriot P320 512GB Internal SSD. This should be plenty of room to play with VMs or Kubernetes.]]></summary></entry><entry><title type="html">What I learned about designing for 3D printing in Fusion 360</title><link href="http://www.declarativesystems.com/2026/05/19/what-I-learned-about-designing-fusion-360.html" rel="alternate" type="text/html" title="What I learned about designing for 3D printing in Fusion 360" /><published>2026-05-19T00:00:00+00:00</published><updated>2026-05-19T00:00:00+00:00</updated><id>http://www.declarativesystems.com/2026/05/19/what-I-learned-about-designing-fusion-360</id><content type="html" xml:base="http://www.declarativesystems.com/2026/05/19/what-I-learned-about-designing-fusion-360.html"><![CDATA[<p>Just some notes…</p>

<h2 id="right-tool-for-the-right-job">Right tool for the right job</h2>

<p>Complex meshes for organic models get edited in Blender, not CAD</p>

<h2 id="project-lines-and-faces">Project lines and faces</h2>

<p>Use project (hotkey <code class="language-plaintext highlighter-rouge">p</code>) in sketch mode to project features into a new sketch plane. This is the key to not having a bunch of lines in one sketch and then losing track of what they all mean</p>

<h2 id="dimension-sketches">Dimension sketches</h2>

<p>Use dimension (hotkey <code class="language-plaintext highlighter-rouge">d</code>) to position sketch elements where they are supposed to be. For example, a cable cutout that needs to be 16mm from an adjacent feature can be sketched roughly and then constrained to the correct location</p>

<h2 id="no-filletschamfers-in-sketches">No fillets/chamfers in sketches</h2>

<p>Unless considered part of the design, these should not be in your sketches as they force removal of constraints and make it hard to reuse the sketch later as a projection.</p>

<p>If you already included these features and they are causing problems in downstream sketches, you can delete fillets from the sketch and then use the <code class="language-plaintext highlighter-rouge">extend</code> tool to restore the original sharp corner.</p>

<p>Do the big fillets first and the smaller ones last, to prevent errors.</p>

<h2 id="name-sketches">Name sketches</h2>

<p>Rename sketches to be something useful, eg <code class="language-plaintext highlighter-rouge">cable cutout</code> not <code class="language-plaintext highlighter-rouge">sketch16</code></p>

<h2 id="bodies-vs-components">Bodies vs Components</h2>

<p>Use bodies for simple parts (eg beginner projects), components for assemblies with repeated or moving parts. Starting with an Assembly file instead of a Design file will stop extrude from working.</p>

<h2 id="making-circuit-board-enclosures">Making circuit board enclosures</h2>

<ol>
  <li>Model something to hold the board (horizontal), extrude it</li>
  <li>Sketch on the side of the new object a cool shape and extrude it</li>
  <li>Use “split body” to create a top and bottom shell</li>
  <li>Add cut-outs</li>
  <li>Fillet</li>
</ol>

<p>Spline base + split body works really nicely for this workflow (Inky impression desktop frame).</p>

<p>Examples:</p>
<ul>
  <li><a href="https://www.printables.com/model/1723813-chonky-retro-case-for-duckypad-pro">Chonky Retro Case for DuckyPad-pro</a></li>
  <li><a href="https://www.printables.com/model/1723754-pimoroni-inky-impression-7-desktop-frame-pim773">Pimoroni Inky Impression 7” desktop frame PIM773</a></li>
</ul>

<h2 id="designing-for-3d-printing">Designing for 3d printing</h2>

<ul>
  <li>Heat set inserts = game changer</li>
  <li>If you can tolerate a visible nut, sunken nuts are a very strong fastener. If you’re out of heat set inserts and don’t want the nut visible, they can be superglued in place but are nowhere near as strong</li>
  <li>Use simple tongue-and-groove step joints to keep parts aligned. Sometimes this can effectively replace hardware fasteners too</li>
  <li>Use big smooth corners on the build plate to prevent warping</li>
  <li>Don’t forget tolerances</li>
  <li>Print with basic filament first, and expect to do a few test prints</li>
  <li>No need to hollow-out features to “save plastic” - your’re not, since infill already accounts for this.</li>
  <li>Use the <code class="language-plaintext highlighter-rouge">section analysis</code> tool to see a cut-through of your design’s hidden faces</li>
  <li>Check check check before printing</li>
</ul>

<h2 id="part-strength">Part Strength</h2>
<ul>
  <li>Use classic strong features to strengthen a design, <em>not</em> infill, as it does not help bending resistance.</li>
  <li>Examples: “L” , “I”, “T”, braces</li>
</ul>

<h2 id="tips-and-tricks">Tips and Tricks</h2>

<p>For my next project:</p>
<ul>
  <li>Parameterize dimensions such as <code class="language-plaintext highlighter-rouge">case_clearance</code></li>
  <li>Photograph circuit boards and insert into the drawing, then use as reference for cut-outs, etc</li>
</ul>]]></content><author><name>Geoff Williams</name><email>geoff@declarativesystems.com</email></author><summary type="html"><![CDATA[Just some notes…]]></summary></entry></feed>