<?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-08-02T23:07:00+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">Designing a PCB for assembly</title><link href="http://www.declarativesystems.com/2026/08/02/designing-pcb-for-assembly.html" rel="alternate" type="text/html" title="Designing a PCB for assembly" /><published>2026-08-02T00:00:00+00:00</published><updated>2026-08-02T00:00:00+00:00</updated><id>http://www.declarativesystems.com/2026/08/02/designing-pcb-for-assembly</id><content type="html" xml:base="http://www.declarativesystems.com/2026/08/02/designing-pcb-for-assembly.html"><![CDATA[<h2 id="why">Why?</h2>

<p>I have a <a href="https://github.com/GeoffWilliams/checkinator">Raspberry Pi Zero W project</a> where I wanted to put a bunch of components inside a small 3D printed enclosure to be left running 24/7. I didn’t want to leave components hanging out of a <a href="https://en.wikipedia.org/wiki/Breadboard">breadboard</a> where they could fall out and this wouldn’t fit either.</p>

<p>I tried building something with <a href="https://en.wikipedia.org/wiki/Perfboard">perfboard</a> but ended up just destroying it due to complex wiring, mistakes and poor soldering technique.</p>

<p>It turns out to be simpler to just learn PCB design with <a href="https://www.kicad.org/">KiCad</a> and have my boards made professionally. For my project, I ended up designing a simple through-hole Raspberry Pi HAT that made the wiring dead simple.</p>

<p>Since this was my first time ever using a PCB manufacturer, I added a couple of other projects to my order to see how far I could push my learning:</p>

<ol>
  <li>A simple <a href="https://en.wikipedia.org/wiki/Through-hole_technology">THT</a> red LED</li>
  <li>A fully assembled <a href="https://en.wikipedia.org/wiki/Surface-mount_technology">SMT</a> <a href="https://www.youtube.com/watch?v=B5eWpRFC9Aw">Buck Converter</a></li>
</ol>

<p>There’s a tenuous relationship between PCB assembly and modern software development that I wanted to explore as part of this exercise as well: In the software world, we hear the term <a href="https://about.gitlab.com/blog/the-ultimate-guide-to-sboms/">SBOM</a> all the time in enterprise IT. I got to <a href="/2026/07/18/java-maven-nvd-sbom.html">experience this for myself recently</a>. The <a href="https://en.wikipedia.org/wiki/Bill_of_materials">BOM</a> concept itself is rooted in physical manufacturing and a good practical example is PCB assembly. If you are trying to use a PCB assembly service, you will need to produce a BOM as well as some other files.</p>

<p>Let’s try out <a href="https://jlcpcb.com/">JLCPCB</a> assembly service and see if we can get something working.</p>

<h2 id="the-project">The project</h2>

<p>I picked a 5V USB-C to 3.3V buck converter. It’s a great first project since:</p>
<ul>
  <li>Any electrical engineer understands what I’m trying to build</li>
  <li>Cheap and readily available components</li>
  <li>Complex enough to present a challenge, but high likelihood of success</li>
  <li>Results in a genuinely useful circuit board. I’m planning on using mine next Christmas to replace battery packs on my LED lights</li>
</ul>

<h2 id="the-process">The Process</h2>

<h3 id="step-1---learn-kicad">Step 1 - Learn KiCad</h3>

<p>The <a href="https://www.kicad.org/">KiCad</a> software itself is outstanding and also Free Software (GPLv3). It’s not as hard to use as <a href="https://www.autodesk.com/products/fusion-360/overview">Fusion 360</a> but it does have its own learning curve. For a simple project like this one you could just use “normal” CAD but a specialist tool like KiCad is 100% worth learning as an investment in your own skills and understanding.</p>

<p>There is a lot more to it than this but rough steps needed in KiCad are:</p>
<ol>
  <li>Schematic design</li>
  <li>PCB design</li>
  <li>Design Rules Check</li>
  <li>Export</li>
</ol>

<h3 id="step-2---research-design-and-component-selection">Step 2 - Research, design and component selection</h3>
<p>I spent a lot of time up-front researching how buck converters work and came up with an initial design:</p>

<p><img src="/assets/img/buckconverter_3d_v0.png" alt="initial design" /></p>

<p>To pick components, I used the <a href="https://jlcpcb.com/parts">JLCPCB parts catalog</a> and leaned on ChatGPT <strong>a lot</strong> for validation.</p>

<p>To do the PCB design, I needed accurate <a href="https://www.pcbx.com/article/What-is-SMT-Footprint">footprints</a> so I signed up for EasyEDA (click through while browsing parts) to download the files for the parts I needed. This is much more convenient and less error prone than <a href="https://learn.pcbcupid.com/pcb-design/design/kicad/custom-footprints-in-kicad">designing your own footprints</a>.</p>

<p>I ended up with a directory of parts I like to use in my <code class="language-plaintext highlighter-rouge">$HOME</code> set as an external library via the <code class="language-plaintext highlighter-rouge">Footprint Editor</code> and was then able to use my exact components for board design.</p>

<h3 id="step-3---design-validation">Step 3 - Design validation</h3>

<p>At this point, I could have had the board manufactured, but I wanted to increase my odds of getting it right first time so I <a href="https://www.eevblog.com/forum/beginners/beginner-pcb-design-help-lm2596-buck-converter/msg5689195/">posted a message on EEVblog</a> asking for help. If you haven’t come across <a href="https://www.eevblog.com/">EEVblog</a> before, it’s THE place to talk to passionate electrical engineers.</p>

<p>A couple of days later, I had some really helpful responses and adjusted my design a bit. Reassuringly (and somewhat surprisingly), no one said it was broken at the design level. At this point this project got put on hold <em>for a couple of years</em>.</p>

<h3 id="step-4---export-upload-order">Step 4 - Export, upload, order</h3>

<p>The files needed for manufacturing follow exact formats and are largely based around CSV. They are used for etching, drilling, pick-and-place, etc and they drive the entire manufacturing process. Designers normally produce a zip file containing <a href="https://jlcpcb.com/help/catalog/180-PCB-Files-Preparation">Gerber files</a>, <a href="https://jlcpcb.com/help/catalog/190-PCBA-Files-Preparation">BOM, and Pick-and-Place files</a>.</p>

<p>Following these links leads to a maze of different file formats, export procedures and gotchas. Any of which are capable of blocking manufacturing or worse.</p>

<p>Automating export in the right format for your chosen manufacturer reduces the opportunity for mistakes and speeds up the process too. After some research I settled on using <a href="https://github.com/bennymeg/Fabrication-Toolkit">Fabrication Toolkit</a>. This KiCad plugin basically produces all of the above files in the correct format in just a few clicks. They can then be uploaded directly for manufacture as a <code class="language-plaintext highlighter-rouge">.zip</code>.</p>

<p>After a final <a href="https://jlcpcb.com/blog/how-to-run-design-rule-check">Design Rules Check</a>, I uploaded the files, got a quote and did a couple of “fixes” which bit me a few hours later:</p>
<ol>
  <li>I had to change a couple of parts due to stock availability</li>
  <li>Holes must be listed in the BOM. Pretty sure I cheated and just added directly to the CSV file:</li>
</ol>

<pre><code class="language-csv">H1,MountingHole_3mm,1,Mechanical hole,N/A
H2,MountingHole_3mm,1,Mechanical hole,N/A
H3,MountingHole_3mm,1,Mechanical hole,N/A
H4,MountingHole_3mm,1,Mechanical hole,N/A
</code></pre>

<p>Then I hit the <code class="language-plaintext highlighter-rouge">Order</code> button.</p>

<h2 id="questions-from-the-factory">Questions from the factory</h2>

<p>A little while after ordering I received some questions from the factory in a very polite email:</p>

<blockquote>
  <p>Dear customer,</p>

  <p>Well got your order with many thanks.</p>

  <p>Sorry to bother you, but there is one thing that we want to confirm with you about your PCBA order [REMOVED] before proceeding.</p>

  <p>1. As shown below, seems the packages of the parts ( C2) cannot match the corresponding footprint on the PCB board.</p>

  <p>We have activated the “Replace Part” button for you. Please change the part on your order history. If you want to leave the designator unpopulated, just unclick the “Select” for this part.</p>

  <p>Please kindly note that we cannot replace the file directly for PCBA order. If you want to change the gerber file, we need to cancel the order, then you place new order with new file.</p>

  <p>2. As shown below, seems the packages of the parts ( J2) cannot match the corresponding footprint on the PCB board.  There’s risk of weak connection, bad soldering, tombstoning. We have 4 suggestions for you.  Please make a choice.</p>

  <p>A: Open the access of “replace parts” for you to change part on order history.</p>

  <p>B: Leave the mismatched part unpopulated and refund and cost of part. We cannot ship the unpopulated part to customer due to customs clearance issue.</p>

  <p>C: Cancel the order for you to redesign the footprint on PCB. Because we cannot replace the file directly for PCBA order. If you want us to refund the payment to JLC balance(not available if customer pay in EUR €). Please set the JLC balance in your account.  https://jlcpcb.com/help/article/how-to-set-up-refunds-to-jlc-balance</p>

  <p>D: Take the risk to solder the current component and will not complaint.</p>

  <p>Your early reply will be highly appreciated, thank you so much!</p>

  <p>Best regards,
JLCPCB Team</p>
</blockquote>

<p>Capacitor footprint:</p>

<p><img src="/assets/img/jlcpcb_bad_footprint_1.png" alt="bad footprint #1" /></p>

<p>Jumper footprint:</p>

<p><img src="/assets/img/jlcpcb_bad_footprint_2.png" alt="bad footprint #2" /></p>

<p>It turns out I selected a replacement capacitor that looked the same to my novice eye but had a different footprint. The header pins were also completely different.</p>

<p>Finding in-stock replacements matching the submitted design files was actually the hardest part of this entire project. Matching the KiCad generic footprint <code class="language-plaintext highlighter-rouge">Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical_SMD_Pin1Left</code> to an exact JLCPCB part was not (for me at least) a quick and easy process. After an intense help session with ChatGPT, I settled on <a href="https://jlcpcb.com/partdetail/DEALON-DZ254S_11_0248/C5160785"><code class="language-plaintext highlighter-rouge">C5160785</code></a> as a replacement.</p>

<p>The next day, I got another very polite email:</p>

<blockquote>
  <p>Dear customer,</p>

  <p>Sorry to bother you.</p>

  <p>For C9939, we are currently short of 5 pieces for the assembly of this order.</p>

  <p>Sorry for the inconvenience. May I ask if you need to replace it with other components? If so, could you check if there is some replacement available in our parts lib? https://jlcpcb.com/parts/</p>

  <p>Looking forward to hearing from you.</p>

  <p>Best regards,</p>

  <p>JLCPCB Team</p>
</blockquote>

<p>I settled on <a href="https://jlcpcb.com/partdetail/73539-SMSD1306470MT/C72428">C72428</a> <code class="language-plaintext highlighter-rouge">2.6A 4.5A 47uH 86mΩ Unshielded Inductor ±20% SMD,18.5x15.2mm Power Inductors ROHS</code> as a replacement part.</p>

<p>Interestingly, the replacement selection at this stage was just done via email instead of the replacement part flow. Presumably this means if I click the re-order button this final change would likely not have been captured. This is probably “fine” for small projects when parts come back into stock but results in a materially different board. This is a problem if you are trying to test, scale or certify a product.</p>

<p>In the software world, we call this <a href="https://reproducible-builds.org/">Reproducible Builds</a> - eg, do I get the exact same board with the same components if I click <code class="language-plaintext highlighter-rouge">Reorder</code>. I highly doubt that in this case, so due diligence means updating the KiCad files with the replacement choices that were made. A more robust approach would have been to cancel the order at the first email, fix the design and resubmit. This proves that all changes have been captured accurately.</p>

<p>Thankfully, the next and final email I received from JLCPCB was a shipping notification.</p>

<h2 id="did-it-work">Did it work?</h2>

<h3 id="simple-led-pcb">Simple LED PCB</h3>
<p>First I assembled the through-hole LED. Hooking up 2x AA batteries made the light come on:</p>

<p><img src="/assets/img/jlcpcb_simple_led.jpg" alt="Simple LED" /></p>

<h3 id="checkinator">“Checkinator”</h3>
<p>The through-hole “checkinator” Raspberry Pi hat also looked great, although I goofed the silkscreening and missed some vital labels. More on this project board another day:</p>

<p><img src="/assets/img/jlcpcb_checkinator.jpg" alt="checkinator" /></p>

<h3 id="buck-converter">Buck Converter</h3>

<ul>
  <li><strong>FIRE DANGER, especially with loads &gt; 1A</strong>
    <blockquote>
      <p>Also D1 looks a bit small, I think it may get too hot. 
Definitely not going to get 3A output with a 3A diode. I’d go with 6A and in a larger package, like SMC.
But ok to test with a 3A diode if the output current is kept at like 1A or so</p>
    </blockquote>
  </li>
  <li>I don’t suggest that anyone attempts to manufacture this board from my KiCad files</li>
  <li>Note intended application: Power small LED string normally driven by 2x AA batteries</li>
</ul>

<p>The buck converter board also looked great. All parts were securely attached and everything was neat and tidy.</p>

<p><img src="/assets/img/jlcpcb_buck_converter.jpg" alt="buck converter pcba" /></p>

<p>The final part of this saga was to crimp dupont connectors onto my old LED battery pack lights and hook-up a laptop USB-C power supply. To my surprise it worked:</p>

<p><img src="/assets/img/jlcpcb_buck_leds_working.jpg" alt="it works!" /></p>

<p>I left it plugged in for hours, nothing got hot and it didn’t burn <strong>my</strong> house down. All 3 designs worked. I call this a success!</p>

<p>After cranking out a <a href="https://www.printables.com/model/1797686-simple-electronics-enclosure">3D printed case in Fusion 360</a> and spending an afternoon assembling cases, I’m now all set for Christmas 2026. I have five of these circuit boards since that’s the minimum order quantity. All boards were tested and worked.</p>

<p><img src="/assets/img/jlcpcb_done.jpg" alt="all done" /></p>

<h2 id="how-much-did-it-cost">How much did it cost?</h2>

<p>The total order came to about $100 USD and about 30% of this cost was DHL shipping to Australia:</p>

<p><img src="/assets/img/jlcpcb_invoice.jpg" alt="invoice extract" /></p>

<p>The PCB assembly service <em>is</em> a lot more expensive than just producing a bare PCB. In my case the extra cost is worth it vs doing a fiddly job on the kitchen bench. I’m positive that putting effort into choosing cheaper parts would have brought down the cost considerably too.</p>

<p>The caveat is that with an MOQ of 5, if you just want a single assembled board, you need to pay 5x the cost vs what you actually want. For a bare PCB this waste can be under $1 per board but an assembled PCB is orders of magnitude more expensive and becomes cost-prohibitive once you’re beyond the realm of “toy” projects.</p>

<h2 id="how-long-did-it-take">How long did it take?</h2>

<ul>
  <li>Fabrication, including all correspondence, took under 6 days. Production was blocked waiting for response from me for around 4 hours total</li>
  <li>DHL Shipping to NSW, Australia took 3 days: Posted Saturday night, arrived first thing Tuesday morning</li>
  <li>Less than 9 days to go from design upload to boards in-hand. Order was made on a Sunday afternoon</li>
</ul>

<h2 id="what-did-i-learn">What did I learn?</h2>

<p><strong>This is a learning exercise, not a useful product</strong></p>

<p>The discussion on EEVblog continued while I got caught up in a ton of side quests for a couple of years, highlighting some changes that would make a better board.</p>

<p>At least one forum reply talked about excessive heat and therefore <strong>FIRE DANGER</strong>, although this seems to be more in connection with higher loads vs intended use of small string of LEDs. RF noise was also mentioned, so it’s likely this little board could cause a nuisance or worse.</p>

<p>In my own testing, nothing measured more than 5C above ambient temperature after an hour or so, but I’m still loath to leave these <em>Christmas Lights</em> unattended in case of returning to a nasty surprise (fire), although everything “seems fine”.</p>

<p>One of the main things I learned in this process was just how nice the assembly service is.</p>

<p>My soldering technique has <a href="https://groups.google.com/g/pidp-11/c/ue9SmMnIqlE?pli=1">caused me big problems</a> in the last couple of years. It’s hard for me to do since I don’t have a dedicated workspace, microscope, etc and if I’m honest, I don’t even particularly enjoy doing it either.</p>

<p>Next time I have a requirement for something beyond <a href="https://github.com/geoffWilliams/formpie">a mess of dupont connectors</a> and <a href="https://en.wikipedia.org/wiki/I2C">I2C</a>, I would consider an assembly service without hesitation.</p>

<p>Additional lessons:</p>
<ul>
  <li>Read and re-read any relevant datasheets</li>
  <li>Check footprints carefully</li>
  <li>Don’t trust AI-recommended replacement parts. It’s very hard even for humans to download an accurate footprint, so ChatGPT and Claude have no chance</li>
  <li>For projects you care about, it’s best to cancel and resubmit designs so footprints can be more easily checked in KiCad and all changes captured</li>
</ul>

<h2 id="how-does-real-manufacturing-relate-back-to-sbom">How does real manufacturing relate back to SBOM?</h2>

<p>For the nerds reading this: There are some subtle but important differences.</p>

<p>The design files that get uploaded to a manufacturer are used to literally drive the production process. The CSV BOM files are used to select individual components for pick-and-place, etc. Our SBOM actually does the opposite - it’s the libraries our build tool identified <em>during</em> the build process.</p>

<p>At least in the world of Java development, <a href="https://maven.apache.org/">Maven</a> uses <a href="https://maven.apache.org/pom.html"><code class="language-plaintext highlighter-rouge">pom.xml</code></a> to drive the build process. Resolving <a href="https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html">Dependencies between libraries</a> (eg library <code class="language-plaintext highlighter-rouge">A</code> requires libraries <code class="language-plaintext highlighter-rouge">B</code>, <code class="language-plaintext highlighter-rouge">C</code> and <code class="language-plaintext highlighter-rouge">D</code>) is a key Maven feature and this concept simply doesn’t exist when using the PCB assembly service: You don’t include an <a href="https://en.wikipedia.org/wiki/Integrated_circuit">IC</a> in a BOM and automatically get a bunch of resistors and diodes included - you have to specify exactly what parts you want to use up-front.</p>

<p>On the manufacturing side, the inverse is true: We can produce boards with parts <em>not</em> in the BOM, with replacement parts discussed on an email thread. It’s really up to the designer to handle this process diligently if it’s important for a project.</p>

<p>With that said, SBOM is a sound security and business continuity practice as <strong>part of</strong> producing secure software. Most programmers don’t know or care about such subtleties, but taking the time to manufacture something from scratch has been a fascinating glimpse into how real-world terms get appropriated by the software industry.</p>

<h2 id="where-are-the-design-files">Where are the design files?</h2>

<ul>
  <li><a href="https://github.com/GeoffWilliams/usb_buck_converter">KiCad files</a></li>
  <li><a href="https://www.printables.com/model/1797686-simple-electronics-enclosure">3D printed enclosure</a></li>
</ul>

<h2 id="thanks-and-acknowledgements">Thanks and Acknowledgements</h2>

<p>My sincerest thanks to everyone who responded to my forum post, especially <code class="language-plaintext highlighter-rouge">mareks</code>, and to <code class="language-plaintext highlighter-rouge">Mia</code> and <code class="language-plaintext highlighter-rouge">Angel</code> at JLCPCB for the help with my order.</p>

<p>This post is not sponsored. I paid for my circuit boards with my own money.</p>

<h2 id="eevblog-forum-post">EEVblog forum post</h2>

<p>For posterity, my original post:</p>

<blockquote>
  <p>Hi,</p>

  <p>I’ve been trying to teach myself basic PCB design so that I can make myself a little Raspberry Pi hat to flash some lights. After spending the weekend burning protoboard I decided it would be easier and better to just dive in to PCB design. I don’t have an electronics or maths background but I’ve watched a lot of youtube and have learnt the basics of Kicad after work this week.</p>

  <p>So far I made a little LED through-hole board which has a good chance of working on first attempt but I wanted something a little more challenging before returning to my original problem. I have some LED Christmas lights that take 2x AA batteries and I had already looked at buying a USB buck convertor to step down 5v to 3.3v for these off Aliexpress but I decided to build my own for practice instead.</p>

  <p>I was hoping someone on this forum could tell me if my learning/design process is on the right lines and ideally have a quick look at the PCB I designed and tear it to shreds - that’s how you learn right.</p>

  <p>What I’ve tried so far:</p>

  <ol>
    <li>Random youtube videos on Kicad</li>
    <li>Stay up late tooling round and building that Raspberry Pi hat. I created a PCB that I’m 99% sure would not work at all as some pins marked 3.3V on the schematic were somehow all 5V in PCB designer. Pretty sure I shorted something. Give up and go back to basics</li>
    <li>Design a little LED circuit. Looks good, I’m going to make this</li>
    <li>Spend focussed time learning about Kicad on youtube. The best vids I found were from “PCB Cupid”</li>
    <li>Get a good explanation of what a buck converter is so I know what I’m building <a href="https://www.youtube.com/watch?v=B5eWpRFC9Aw">https://www.youtube.com/watch?v=B5eWpRFC9Aw</a></li>
    <li>Lots of googling for buck converter circuits. Found one that looked easy but also nothing like what Louis explained. That’s because it was just a voltage regulator and I would have built the wrong circuit if I hadn’t done some research</li>
    <li>Found the TI LM2596 SIMPLE SWITCHER referenced in a few places and decided to use this. Read the datasheet and tried to build the circuit listed under “Typical Application”. This is for 5v though, not 3.3v. According to Chat GPT I can just change the IC to the 3.3v version and leave the inductor and capacitors at the same value. Find missing components on snapeda and import into Kicad</li>
    <li>Design a schematic and add USB-C power connector with the CC pins grounded through 5.1K resistors. Electrical Rules Check passing</li>
    <li>I want to have this board fully assembled with SMD components by JLCPCB but the diode and coil parts from datasheet are not in stock on <a href="https://jlcpcb.com/parts/all-electronic-components">https://jlcpcb.com/parts/all-electronic-components</a> so I spent more time looking at specs that I barely understand and then asking Chat GPT if my substitution is OK</li>
    <li>Build PCB from schematic, not fully happy with it</li>
    <li>Do some research on PCB design, Read Dave’s book: <a href="https://www.scs.stanford.edu/~zyedidia/docs/pcb/pcb_tutorial.pdf">https://www.scs.stanford.edu/~zyedidia/docs/pcb/pcb_tutorial.pdf</a>, watch some EEVBLOG vids</li>
    <li>Delete PCB and start again. Try to layout components like the datasheet says, add big traces and copper fill areas. Design Rules Check passing</li>
  </ol>

  <p>I’ve learnt a lot in this process. My overall goal is just to be able to make simple PCBs for Raspberry Pi or other simple circuits for fun, in the future. I’m not planning on changing careers or building really complicated things.</p>

  <p>So on to my questions:</p>

  <p>Components OK?</p>
  <ul>
    <li>33uH inductor AIAP-02-330K</li>
    <li>Input capacitor 680uF EEE-FK1J681AM</li>
    <li>Output capacitor 220uF TPSE227M016R0100</li>
    <li>Diode 30V 3A SS33 (THT - could not find an in-stock SMD one)</li>
    <li>Apart from the USB connector, I only built what was in the datasheet. Is anything else required?
      <ul>
        <li>All good for intended application?: Connect to old phone charger and hook up a small LED string and possibly any other random low power 3.3V DC needs in the future</li>
        <li>What are the chances of my PCB design working?</li>
      </ul>
    </li>
  </ul>

  <p>I haven’t had anything made yet so just asking for any suggestions before I see if this blows up. :-DD</p>

  <p>Thanks for reading all this!</p>

  <p>Cheers,
Geoff</p>
</blockquote>]]></content><author><name>Geoff Williams</name><email>geoff@declarativesystems.com</email></author><summary type="html"><![CDATA[Why?]]></summary></entry><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>

<h2 id="system-still-running-planning-moving-to-a-new-access-point-then-use-nmcli">System still running? Planning moving to a new Access Point? Then use <code class="language-plaintext highlighter-rouge">nmcli</code></h2>

<p>If your not locked out of your system at the moment and are planning on switching WIFI access points, you can just add the new one with <code class="language-plaintext highlighter-rouge">nmcli</code> while you still have access.</p>

<p><strong>Example - adding an out-of-range WIFI:</strong></p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nmcli connection add <span class="nb">type </span>wifi ifname wlan0 con-name theSSID ssid <span class="s2">"theSSID"</span>
nmcli connection modify theSSID wifi-sec.key-mgmt wpa-psk
nmcli connection modify theSSID wifi-sec.psk <span class="s2">"password123"</span>
nmcli connection modify theSSID connection.autoconnect <span class="nb">yes</span>
</code></pre></div></div>

<p>If it worked, you will see the new connection listed in NetworkManager:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># nmcli connection show
NAME                  UUID                                  TYPE      DEVICE 
netplan-wlan0-ASIO-u  6fbd4250-1205-3a84-8c29-6096c50f9538  wifi      wlan0  
lo                    c83b8865-52cd-44d2-bce1-df4ea16bdd3f  loopback  lo     
theSSID               87e14c2a-0372-4484-a41a-f2de6afd079e  wifi      --     
netplan-eth0          75a1216a-9d1a-30cd-8aca-ace5526ec021  ethernet  --   
</code></pre></div></div>]]></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></feed>