BrixIT BlogRandom blog posts from Martijn Braamhttps://blog.brixit.nl/https://blog.brixit.nl/image/w300/static//static/files/blog.brixit.nl/1669734482/favicon.pngBrixIT Bloghttps://blog.brixit.nl/Thu, 03 Apr 2025 16:56:41 -000060Building a browser game based on KiCadhttps://blog.brixit.nl/building-a-browser-game-based-on-kicad/121Martijn BraamThu, 03 Apr 2025 16:56:41 -0000<p>I've been making boards in KiCad for a while now. I really enjoy figuring out how to route all the components in the PCB editor, especially the weird "hard" things like differential high speed signals. I'm probably not very good at it but so far the margins for the stuff I'm designing is wide enough that it works anyway.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1743695205/kicad-swigglies.PNG" class="kg-image"></figure> <p>Every time I'm working on this I feel like this would make a neat puzzle game, but I don't feel like building an entire PCB editor in a game engine to make this concept work. So I took a page from the Minecraft book and turned it into cubes. Making the problem discrete into a grid makes it way easier to reason about and a more logical game.</p> <p>To make the whole thing easy to build I decided to go with a simple browser game. No need to deal with much platform support there and I don't have to either use one of the big game engines or write one myself. I debated for a few seconds whether I should go with TypeScript or just vanilla JS. In the end I wrote the entire thing in plain javascript.</p> <h2>The game</h2> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1743695605/image.png" class="kg-image"><figcaption></figcaption></figure> <p>The whole game area is drawn as a <code><canvas></code> element and the javascript code generates the DOM elements for a few of the user interface elements.</p> <p>The goal of the game is to connect all the source blocks to the sink blocks of the same color. The source blocks are the arrows pointing up and the sink blocks are the arrows pointing down. They probably need to have less similar icons but that's where my creativity ended.</p> <p>The connection for every color needs to be exactly the same length. In the solution above I've already failed this since the orange connection reached the end already at 11 ticks before the rest of the signals are even halfway. Reaching the sink at the same time is the bare minimum to have a valid solution, to get a higher score though the signal integrity needs to be as high as possible.</p> <p>Signal integrity is highest when the signals are traveling together like in the green/blue line. Every tick every signal is checked for neighbors and if it has a neigboring signal it will increase the quality, if it has two neighboring signals it will increase the quality even more. This is balanced in a way that having no neighbors at all for the signal will quickly drop all the points of the connection so you can only do that for small segments.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1743696647/image.png" class="kg-image"></figure> <p>This is a solution to the initial level I made, basically the bare minimum to have a valid solution and it generates 2482 points. This is also the initial version I released on mastodon some days ago so a few people have tried to beat that score, and it has been done easily. The current high score for this level is now 4000 points by @viciouss.</p> <p>The current dev version of the game can be played at <a href="https://brixitcdn.net/game/">https://brixitcdn.net/game/</a></p> <h2>Internals</h2> <p>The internals for the game are not terribly exciting. It's mostly full of hooking javascript events to gamecode like this:</p> <div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">self</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">;</span> <span class="k">this</span><span class="p">.</span><span class="nx">canvas</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;mouseenter&#39;</span><span class="p">,</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">self</span><span class="p">.</span><span class="nx">hover</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">true</span><span class="p">;</span> <span class="w"> </span><span class="nx">self</span><span class="p">.</span><span class="nx">dirty</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">true</span><span class="p">;</span> <span class="p">});</span> <span class="k">this</span><span class="p">.</span><span class="nx">canvas</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;mouseleave&#39;</span><span class="p">,</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">self</span><span class="p">.</span><span class="nx">hover</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">false</span><span class="p">;</span> <span class="w"> </span><span class="nx">self</span><span class="p">.</span><span class="nx">dirty</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">true</span><span class="p">;</span> <span class="p">});</span> <span class="k">this</span><span class="p">.</span><span class="nx">canvas</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;mousemove&#39;</span><span class="p">,</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">(</span><span class="nx">event</span><span class="p">)</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">bounds</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">self</span><span class="p">.</span><span class="nx">canvas</span><span class="p">.</span><span class="nx">getBoundingClientRect</span><span class="p">();</span> <span class="w"> </span><span class="nx">self</span><span class="p">.</span><span class="nx">hover</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">true</span><span class="p">;</span> <span class="w"> </span><span class="nx">self</span><span class="p">.</span><span class="nx">mouse</span><span class="p">.</span><span class="nx">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">event</span><span class="p">.</span><span class="nx">clientX</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nx">bounds</span><span class="p">.</span><span class="nx">left</span><span class="p">;</span> <span class="w"> </span><span class="nx">self</span><span class="p">.</span><span class="nx">mouse</span><span class="p">.</span><span class="nx">y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">event</span><span class="p">.</span><span class="nx">clientY</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nx">bounds</span><span class="p">.</span><span class="nx">top</span><span class="p">;</span> <span class="w"> </span><span class="nx">self</span><span class="p">.</span><span class="nx">dirty</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">true</span><span class="p">;</span> <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">self</span><span class="p">.</span><span class="nx">mouseHeld</span><span class="p">)</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">mouse</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="nb">Math</span><span class="p">.</span><span class="nx">floor</span><span class="p">(</span><span class="nx">self</span><span class="p">.</span><span class="nx">mouse</span><span class="p">.</span><span class="nx">x</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="mf">16</span><span class="p">),</span><span class="w"> </span><span class="nb">Math</span><span class="p">.</span><span class="nx">floor</span><span class="p">(</span><span class="nx">self</span><span class="p">.</span><span class="nx">mouse</span><span class="p">.</span><span class="nx">y</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="mf">16</span><span class="p">)];</span> <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">mouse</span><span class="p">[</span><span class="mf">0</span><span class="p">]</span><span class="w"> </span><span class="o">!==</span><span class="w"> </span><span class="nx">self</span><span class="p">.</span><span class="nx">lastMouse</span><span class="p">[</span><span class="mf">0</span><span class="p">]</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nx">mouse</span><span class="p">[</span><span class="mf">1</span><span class="p">]</span><span class="w"> </span><span class="o">!==</span><span class="w"> </span><span class="nx">self</span><span class="p">.</span><span class="nx">lastMouse</span><span class="p">[</span><span class="mf">1</span><span class="p">])</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">self</span><span class="p">.</span><span class="nx">lastMouse</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">mouse</span><span class="p">;</span> <span class="w"> </span><span class="nx">self</span><span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="nx">mouse</span><span class="p">[</span><span class="mf">0</span><span class="p">],</span><span class="w"> </span><span class="nx">mouse</span><span class="p">[</span><span class="mf">1</span><span class="p">]);</span> <span class="w"> </span><span class="p">}</span> <span class="w"> </span><span class="p">}</span> <span class="p">});</span> </pre></div> <p>The game uses a <code>dirty</code> flag to repaint the game only when there's actual changes, because on the naive <code>requestAnimationFrame()</code> approach the thing still pins an entire core just drawing a basic grid for some reason.</p> <p>The entire state of the game is stored in a simple two dimensional array where the contents are either null or an <code>TCBlock()</code> object that stores the color, type and state of that pixel. Most the code in the game deals with implementing drawing lines inside this array.</p> <p>When the verify button is pressed a loop is started where every 200ms the entire playfield is simulated. On the initial step all the source blocks will get a "charge" and then every step that charge gets propagated in all cardinal directions if a block of the same color exists in that location. The game tracks every pixel that has had a charge before so it is not possible to create an infinite loop with this mechanic. The simulation terminates when either all charges have been absorbed by a sink block or a simulation step doesn't change the state of the game anymore.</p> <p>The most interesting part is the loading/saving system. I wanted to have the entire level stored in the URL so it's easy to share levels made in the game. This way I also don't need any server side code and the entire thing can just be static HTML, CSS and javascript.</p> <p>My initial implementation of saving the game was exporting the state array to JSON and then base64 encoding that to an URL fragment. It turns out that this already consumes 400 characters for just the initial level with 8 blocks on it set. The size explodes when there's actually lines drawn in the level.</p> <p>My first fix for this was adding brotli.js into the codebase and compressing the json blob before encoding it as base64. This helps a lot and brings the size down to 80 characters for the initial state, but it's still way too large. The solution is obviously to use some actual engineering instead of just throwing things into JSON.stringify here.</p> <p>There are only four numbers that need to be saved for every block that is set:</p> <ul><li>The X and Y coordinate</li> <li>The color, every block has a hex string as color.</li> <li>The block type: &quot;source&quot;, &quot;sink&quot;, &quot;wire&quot;, &quot;wall&quot;.</li> </ul> <p>For the color I added a lookup table to the game and defined a series of colors so I can just store an index into that array. For the block type I simply stored a single character for the types: "S", "s", "w" and "o" for the four block types. The coordinates are also never larger than 256 in any direction so this means I can pack every block into 4 bytes. With this improvement my initial state was down to 32 bytes and with my test solution it was around 200 bytes.</p> <p>I decided to throw brotli against this problem again to fix this. The initial state of 32 bytes gets compressed with brotli to... 36 bytes... so that doesn't help at all. But the 200 byte field gets compressed to ~100 bytes which is a nice improvement. It can still be better though...</p> <p>I don't actually need that many colors, I probably don't need more than 4 types in total and the coordinates are nowhere near 256. I slightly shrunk the play area from 640x480 to 512x480 so the largest coorinate in my 16x16 grid becomes 32. This means I can pack the X and Y coorinate together in just 10 bits. If I limit the other values to 8 maximum I can store that in 6 bits together giving me the state for a single block packed into 2 bytes instead of 4, halving the size of the savegames again.</p> <p>This is what's now actually in use in the demo to save the initial states and what makes the "export" button work that puts your entire solution in the URL.</p> <div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">buf</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nb">ArrayBuffer</span><span class="p">(</span><span class="nx">result</span><span class="p">.</span><span class="nx">length</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mf">2</span><span class="p">);</span> <span class="kd">const</span><span class="w"> </span><span class="nx">bufView</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nb">Uint8Array</span><span class="p">(</span><span class="nx">buf</span><span class="p">);</span> <span class="nx">result</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">item</span><span class="p">,</span><span class="w"> </span><span class="nx">i</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">bufView</span><span class="p">[(</span><span class="nx">i</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mf">2</span><span class="p">)]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">item</span><span class="p">[</span><span class="mf">0</span><span class="p">]</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="mh">0x1F</span><span class="p">)</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="p">(</span><span class="nx">item</span><span class="p">[</span><span class="mf">2</span><span class="p">]</span><span class="w"> </span><span class="o">&lt;&lt;</span><span class="w"> </span><span class="mf">5</span><span class="p">);</span> <span class="w"> </span><span class="nx">bufView</span><span class="p">[(</span><span class="nx">i</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mf">2</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mf">1</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">item</span><span class="p">[</span><span class="mf">1</span><span class="p">]</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="mh">0x1F</span><span class="p">)</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="p">(</span><span class="nx">item</span><span class="p">[</span><span class="mf">3</span><span class="p">]</span><span class="w"> </span><span class="o">&lt;&lt;</span><span class="w"> </span><span class="mf">5</span><span class="p">);</span> <span class="p">});</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">&quot;Packed:&quot;</span><span class="p">,</span><span class="w"> </span><span class="nx">buf</span><span class="p">.</span><span class="nx">byteLength</span><span class="p">);</span> <span class="kd">const</span><span class="w"> </span><span class="nx">compressed</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">brotli</span><span class="p">.</span><span class="nx">compressArray</span><span class="p">(</span><span class="nx">bufView</span><span class="p">,</span><span class="w"> </span><span class="mf">11</span><span class="p">);</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">&quot;Compressed:&quot;</span><span class="p">,</span><span class="w"> </span><span class="nx">compressed</span><span class="p">.</span><span class="nx">length</span><span class="p">);</span> <span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">hash</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">btoa</span><span class="p">(</span><span class="nb">String</span><span class="p">.</span><span class="nx">fromCharCode</span><span class="p">.</span><span class="nx">apply</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span><span class="w"> </span><span class="nx">compressed</span><span class="p">));</span> </pre></div> <p>The <code>result</code> array already contains the data compressed into the <code>[[x,y,colorIdx,typeIdx]]</code> format so that's run through the byte packing code, the compressor and finally the base64 encoder to produce the savegame.</p> <h2>Conclusion</h2> <p>It was suprisingly easy to make all of this, I have of course already used all the seperate components I've used here before so that saves a lot of research. In total I probably spend less than a day to implement all of this, including writing this blog post.</p> <p>The game itself seems reasonably fun. It can probably be a bit more difficult and the level editing can certainly be a lot better.</p> <p>The mechanic that is stolen from PCB design in KiCad seems to work pretty well and the scoring mechanism certainly points you into a solution that closely mirrors the advice you get for routing differential signals: Length match the pairs, add timing wiggles close to the source of the features that change the timing to make the differential signalling work the best.</p> <p>You can play the game at <a href="https://brixitcdn.net/game/">https://brixitcdn.net/game/</a> and the level editor is available at <a href="https://brixitcdn.net/game/#editor">https://brixitcdn.net/game/#editor</a></p> <p>Source code for this is at <a href="https://git.sr.ht/~martijnbraam/TimeCode">https://git.sr.ht/~martijnbraam/TimeCode</a></p> Megapixels 2.0: Small fixes and GTK breakagehttps://blog.brixit.nl/megapixels-2-0-progress/119MegapixelsMartijn BraamFri, 28 Mar 2025 20:52:26 -0000<p>Looking back it seems like making an alpha release of Megapixels 2.0 was a great choice. The various components that make up Megapixels have been through the packaging steps at Mobian for example which brought some light to issues that would improve the packaging. A lot more eyes have hit the code in this release than the random stuff thrown to git and I'm really happy I've received a lot of improvements from a bunch of developers.</p> <p>When I started working on Megapixels it was my first C codebase, when I started working on libmegapixels/libdng those were the first times figuring out how do do C libraries. In the years I've been working on this codebase I've leared a whole lot, mostly around cleaner code and seeing merge requests that fix up minor issues in the code are a great reference for figuring out what the idiomatic way is of writing specific pieces of code.</p> <p>Of course quite a few of these are minor memory safety violations and there has been a few un-free()'d resources around within Megapixels, but in the end running free on a few bytes before quitting the app or letting Linux release that memory doesn't make that much of a difference, most of the effort has been going through making sure the main image processing loop doesn't leak memory anywhere. If that part leaks memory then it will starts adding up really fast :)</p> <h2>GTK throwing a wrench into the development process</h2> <p>Of course Linux development can't ever run smoothly... there's always something new and exciting to break everything.</p> <p>In the case of Megapixels it has been the NGL backend for GTK4. The 4.17 release made in Februari dropped the GL backend in favor of the NGL and Vulkan renderers. Which is great if you're on the latest and greatest Macbooks.</p> <p>The issue is that GTK now also dropped for GLES 2.0 which means that a lot of older devices are no longer GPU accelerated in GTK4. For Megapixels it's an even bigger issue since the debayering depends on GPU acceleration so it won't run at all if GTK4 doesn't have an OpenGL context anymore.</p> <p>This hardware doesn't even have to be terribly old. For example here's some of the hardware supported by Megapixels 2.0:</p> <table> <tr><th>Device</th><th>GPU</th><th>OpenGL</th><th>OpenGL ES</th></tr> <tr><th>PinePhone</th><td rowspan=2>Mali 400</td><td rowspan=2>OpenGL 2.0</td><td rowspan=2>OpenGL ES 2.1</td></tr> <tr><th>Samsung Galaxy SIII</th></tr> <tr><th>PinePhone Pro</th><td>Mali T860</td><td>OpenGL 3.1</td><td>OpenGL ES 3.1</td></tr> <tr><th>Librem 5</th><td>Vivante GC7000Lite</td><td>OpenGL 2.0</td><td>OpenGL ES 2.x</td></tr> </table> <p>What they all have in common is that they don't really support the latest and greatest OpenGL versions. It's not very easy to get any hard docs on what OpenGL requirements GTK has now, but it seems like it's at least OpenGL 3.3 and there's still references to OpenGL ES 3.0 in the codebase. Which means that for the devices I've been targetting for Megapixels the support simply isn't there anymore.</p> <p>So far there's been a workaround for this by putting Megapixels in a flatpak with a runtime that doesn't have the latest version of GTK4 in it. This is obviously not a long term solution but at least there's <i>some</i> workaround for now. Many thanks to Andrey Skvortsov for <a href="https://gitlab.com/megapixels-org/Megapixels/-/merge_requests/44">creating a flatpak package</a> for Megapixels.</p> <p>There's a build available now on my flatpak repository, you should be able to get your favorite graphical packagemanager frontend to install it with this link: <a href="https://flatpak.brixit.nl/megapixels2.flatpakref">https://flatpak.brixit.nl/megapixels2.flatpakref</a></p> <h2>The future</h2> <p>I'm not entirely sure what a good solution for this mess is. My current feeling is that it's best to not rely on GTK4 anymore because even if somehow a workaround is figured out to make this work, there's always the next GTK issue coming up.</p> <p>Switching to another framework also isn't great, especially since Megapixels just has been through rewrite hell already, it'd be 2026 before we'd have a working camera app again. In theory it should be slightly easier now to make megapixels-qt now with libmegapixels but that only abstracts the device usage, the main magic of Megapixels is the threading mess and OpenGL debayer code that gives it the realtime performance for photography and that would have to be recreated on another platform.</p> <p>Maybe someone has a great idea or a solution for this, I'd love to hear it.</p> After-FOSDEM videobox updateshttps://blog.brixit.nl/after-fosdem-videobox-updates/110ElectronicsMartijn BraamMon, 10 Mar 2025 02:30:20 -0000<p>The FOSDEM video capture box is a custom device for doing all the in-room stuff for the live-streams of the event. It contains a Radxa x4 SBC, a digital audio mixer, an HDMI capture card, some USB chargers and a network switch. Every room that is livestreamed has two of these boxes, one in front that captures the audio and the HDMI signal for the projector. The other box is in the back of the room hooked up to the camera.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1741569207/box-base-ethaf.png" class="kg-image"><figcaption>Wiring diagram for the box internals</figcaption></figure> <p>The network switch is the <a href="https://blog.brixit.nl/making-a-linux-managed-network-switch/">managed network switch </a>I've written about before, but instead of being hooked up to an ARM computer and using the DSA system in Linux to make it appear native it's mostly unmanaged here. There is connectivity between the switch and the radxa only for some basic monitoring.</p> <p>The audio mixer here is also the <a href="https://blog.brixit.nl/digital-audio-mixer-pt-2/">digital audio mixer design</a> I've written about before. It's handling the audio input from the wireless microphones used at FOSDEM and it sends out the audio to the XLR inputs on the camera used for recording.</p> <p>The power board is another custom designed board by Dexter, it takes 12V from the wall and provides the power for all the internal components, it also provides 4 USB charging ports in the front which are electrically isolated from the rest of the system to have the wireless receivers plugged into the chargers without creating ground loops. This boards also has a few auxilary functions like controlling the fans in the box.</p> <p>A talk has also been given about this hardware at FOSDEM itself (at the end of the event after we were sure everything was working smoothly) and is watchable at <a href="https://fosdem.org/2025/schedule/event/fosdem-2025-6832-fosdem-videobox-2025/">https://fosdem.org/2025/schedule/event/fosdem-2025-6832-fosdem-videobox-2025/</a>.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1741573626/image.png" class="kg-image"></figure> <p>To add a bit of irony to the situation this talk about fixing audio quality issues at FOSDEM has bad audio because we decided at the last moment to plug in a spare wireless mic into the system to have mic handoffs and had the gain of the receiver set higher than the box volume could be adjusted.</p> <h2>Next up after FOSDEM</h2> <p>The hardware worked great during the event and now the pressure of having it work at all is gone some extra time has been spend on fixing up issues in the design.</p> <p>One of the more annoying things about the box is that you can hear it in the recordings and in the rooms because the fans are too loud. This is a combination of various design tradeoffs. The first is that the fans are hooked up to an EMC2305 fan controller chip. This chip is also used in a few computers as the internal fan controller. Sadly the automatic control functionality of this chip simply doesn't work (and we checked the Linux kernel driver for it, it also doesn't use the automatic functions). In this design it's only used to read the tachometer signals of the fans and output the PWM signal for the fans.</p> <p>The second issue is that the fans don't have a PWM input so the fan control is done by applying the PWM signal to the power input. Due to this control mechanic and the limitation that the PWM of the fan controller chip has only 8 bit resolution the usable range of control for the fan is reduced to ~6 levels where there's measureable difference in the fan speed, and the only real levels there are off, slow and 4 levels of way too loud.</p> <p>The firmware that ran at FOSDEM had a control loop on the fans that measured the fan RPM and adjusted the PWM signal to keep the fan speed at a preset PWM which due to the limited control resolution just meant the system occasionally started oscillating and the fan speed was ramped up a lot due to the anti-stall mechanism.</p> <p>This has now been replaced by a far simpler control loop that only allows setting "off, low and fast" so it matches reality and not use any tachometer feedback to control this but instead only use that for stall detection.</p> <h2>Audio firmware changes</h2> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1741573087/20250113_0006.jpg" class="kg-image"></figure> <p>The audio mixer at fosdem did not use the USB audio path at all, the audio signals were mixed and then the analog XLR outputs were used to send the audio to the speakers in the room if available, and to the audio input of the camera so the audio is embedded into the HDMI signal that is captured. This is mainly to maintain sync between audio and video through the system. The firmware running at FOSDEM had stability issues when using the USB soundcard functionality of the box which luckily wasn't needed.</p> <p>But these boxes are not only used at FOSDEM, it would be a shame to make custom hardware that is only used 2 days per year. The boxes are loaned out to other conferences to try it out and one of those conferences is FOSSASIA in Bankok. On this conference there is no HDMI camera used for the capture in the rear of the room but instead it's using a webcam, and webcams don't have inputs for audio input. After figuring out that it was the patches to run the Teensy audio board at 48KHz sampling rate instead of 44.1KHz it was decided that those patches should just be ripped out. So far the firmware has been stable with these changes.</p> <h2>VLAN support for the network switch</h2> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1741573105/20250113_0014.jpg" class="kg-image"></figure> <p>The network switch in the box has performed perfectly during the FOSDEM event but none of the fancy features of the switch chip were used. The main thing that was done was reading out the port status so it can be shown on the display on the front of the box.</p> <p>For more flexibility it would be nice to give this more managed switch functions, one of the main things being VLAN support. The basic functionality for the switch is very easy since you just supply the chip with power and it starts switching, figuring out how to control the chip itself is way harder. There's not nearly enough public information available on programming the RTL8367S, the datasheet only covers the registers for port status that I've been using.</p> <p>Using a lot of puzzling and reverse engineering I've figured out how to load a VLAN config into the chip. This is mainly based around bits of info available from other realtek switch chips and some bits of leaked SDK code for similar chips. This means there's now some extra APIs available in the rp2040 code that controls the switch in the box for setting up a static vlan configuration on boot:</p> <div class="highlight"><pre><span></span><span class="c1">// Set the MemberConfig for all ports to 0 and enable tag handling on the ports</span> <span class="n">nsw_vlan_init</span><span class="p">();</span> <span class="c1">// Enable the VLAN processing in the switch</span> <span class="n">nsw_config_vlans</span><span class="p">(</span><span class="nb">true</span><span class="p">);</span> <span class="c1">// Create a VLAN table entry for VLAN 10 with port 1, 2 and 3 as members</span> <span class="c1">// Port 2 and 3 will be untagged on egress</span> <span class="n">nsw_vlan_cfg_t</span><span class="w"> </span><span class="n">vlan10</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="p">.</span><span class="n">vid</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span> <span class="w"> </span><span class="p">.</span><span class="n">mbr</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">BIT</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">BIT</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">BIT</span><span class="p">(</span><span class="mi">2</span><span class="p">),</span> <span class="w"> </span><span class="p">.</span><span class="n">untag</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">BIT</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">BIT</span><span class="p">(</span><span class="mi">2</span><span class="p">),</span> <span class="p">};</span> <span class="n">nsw_vlan_set</span><span class="p">(</span><span class="o">&amp;</span><span class="n">vlan10</span><span class="p">);</span> <span class="c1">// Create a MemberConfig row to ingress untagged traffic on port 2 and 3</span> <span class="c1">// Use MemberConfig index 1 and vlan 10</span> <span class="n">nsw_mc_set</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w"> </span><span class="n">BIT</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">BIT</span><span class="p">(</span><span class="mi">2</span><span class="p">));</span> <span class="c1">// Point the two untagged ports to MemberConfig index 1. This needs to</span> <span class="c1">// happen _after_ setting the ports as member in the MemberConfig itself</span> <span class="n">nsw_port_set_mc</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">);</span> <span class="n">nsw_port_set_mc</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">);</span> <span class="c1">// Set the untagged ports to drop traffic with a vlan tag set</span> <span class="n">nsw_port_vlan_filtering</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="n">PORT_ACCEPT_UNTAGGED_ONLY</span><span class="p">);</span> <span class="n">nsw_port_vlan_filtering</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="n">PORT_ACCEPT_UNTAGGED_ONLY</span><span class="p">);</span> <span class="c1">// Drop untagged traffic from the trunk port</span> <span class="n">nsw_port_vlan_filtering</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="n">PORT_ACCEPT_TAGGED_ONLY</span><span class="p">);</span> </pre></div> <p>There's also half an implementation for reading port statistics. The switch provides the standard port statistics MIB block, I just need to figure out the final bits for mapping the MIB numbers to the right address in memory.</p> <p>The current port status information that's read out of the PHY registers of the switch are equivalent of the data in the <code>ethtool eth0</code> command, the MIB registers contain the information presented in the <code>ethtool -S eth0</code> command. This would great to produce some pretty Grafana charts.</p> <h2>Hardware improvements</h2> <p>The audio board performed decently given the constraints of the FOSDEM environment and the hardware plugged into it, but I'd like it to be a bit more flexible. The XLR inputs right now are designed to accept -10dBu consumer line level signals. Plugging in a cheap microphone doesn't really work since the board does not have enough gain available to crank up the levels in hardware and is also too noisy to just boost the levels in software. It is also possible to clip the inputs using the AVX wireless systems used at FOSDEM which also isn't ideal.</p> <p>The reason for the small dynamic range of the inputs is for the FOSDEM constraints the simplest and most reliable solution was to make an analog frontend that works from the 5V usb power directly and is tweaked line level signals. The initial version of the board had switching regulators on it to produce +12V and -12V power rails for the analog inputs and the filtering for these inputs was not enough. To make some more HiFi inputs I'm now working on some prototypes of better power supplies and have some new designs around better ADCs and better analog frontends.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1741572724/image.png" class="kg-image"><figcaption>Power supply 2.0 test board</figcaption></figure> <p>This design of the power supply reverts back to the original switching regulator but now uses the filtering circuitry used with the 5V powered analog inputs of the FOSDEM hardware to hopefully produce a less noisy signal. The redesign for audio input also replaces the integrated TI codec which has a lot of issues with a dedicated ADC and DAC chip, one of the major improvements here is that the inputs of these ADC chips is already balanced so the board doesn't have any unbalanced audio signals on it anymore.</p> <p>One of the main things that needs figuring out is how to have cheap software control of the input gain, for using cheap wired microphones it would be needed to boost the signal levels <i>a lot</i>, somewhere between 40 and 60 dB of extra gain without too much noise added. There are easy solutions for this, but the easy solutions are never cheap, especially considering you need a copy of the circuit for every input channel.</p> <p>Having some better input circuitry would make the FOSS digital audio mixer a lot more widely usable, there's many situations where I really would've used a small audio mixer to do some simple routing.</p> <h3>See also</h3> <p>Want to read more about the FOSDEM hardware? Other video team members have also written some blog posts about other details:</p> <p><a href="https://blog.ubii.me/2025/02/13/fosdem-mixers/">https://blog.ubii.me/2025/02/13/fosdem-mixers/</a></p> <p><a href="https://vasil.ludost.net/blog/?p=3494">https://vasil.ludost.net/blog/?p=3494</a></p> <p></p> BodgeOS pt.4: A working browserhttps://blog.brixit.nl/bodgeos-pt-4-a-working-browser/109BodgeOSMartijn BraamSat, 25 Jan 2025 11:35:26 -0000<p>After the previous post about bringing up Sway I spend a bit of time packaging random system components you'd expect for a desktop system. Mainly building GTK so I can have some actual applications running. This is mainly pretty relaxing work. Find a component to build, figure out the dependencies, package them one by one by figuring out the required commands and making a tiny build script.</p> <p>Occasionally the zen of packaging gets disturbed by having to figure out why some build system does something weird, like really wanting to put files in /usr/lib64 instead of /usr/lib while in the previous build it did not do this.</p> <p>It turns out this is because some build systems perform auto detection of the host system to decide things like "what folder is a good place for the libraries". In this case it was <code>cmake</code> that has autoconfig behavior for this if you don't explicitly define that libs go in the <code>lib</code> folder. At some point installing a broken rebuild of glibc on the host system had changed lib64 back from a symlink to a regular folder again and all builds after that were broken.</p> <h2>The path to Firefox is paved with many dependencies</h2> <p>For my goal of running Firefox I have to deal with several dependencies:</p> <ul><li>GTK 3 and the components to make that work like Pango and Cairo.</li> <li>A whole assortment of audio, video and image codecs, these can probably be skipped by manipulating the Firefox buildsystem, but it is probably easier to package all of these instead.</li> <li>Nodejs is also a dependency for building Firefox since the javascript engine in Firefox itself can&#x27;t be used to run the javascript components in the build system.</li> <li>WASI for having WebAssembly support.</li> <li>Rust for large chunks of the Firefox codebase.</li> <li>Extra libraries like the Netscape Portable Runtime (nspr) and the libraries for other system features like Alsa and Pulse.</li> </ul> <p>Luckily parts of this have already been packaged as part of getting my Sway desktop working. Many dependencies for GTK 3, WASI and Rust are already packaged for either Mesa or Swaybar, but I had to finally figure out how to bring up Rust in my distro.</p> <p>This is quite hard in theory because rustc is written in Rust so I need older versions of Rust to build the current version of Rust, and older versions for that again and again until you reach the point where rustc is written in C again. Instead of doing all that I opted to go for the easy road. I <code>curl | sh</code>'ed a functional Rust compiler into my host system with the instructions for rustup.rs and then used that compiler to build my packaged Rust. This was all surprisingly smooth and easy.</p> <p>Getting NodeJS functional was a bit more painful. This is mainly because it takes forever to build which means it takes forever to fix things while packaging. The same issue with the Firefox build itself actually. The worst thing I've encountered while packaging things is things that take a lot of time to build and don't have the configure script check <i>all</i> the dependencies it needs. I might have been annoyed with autoconf wasting a lot of time at the start of every build of every small package with checks that seem to be completely useless, but the time wasted there does not compare at all to the time wasted in building Firefox and having it crash an hour into the build complaining it's missing a dependency.</p> <h2>Desktop audio</h2> <p>One of the dependencies for Firefox is also the various audio libraries, which in turn means I have to bring up audio on my distro. I spend a moment to figure out wether it would be a PulseAudio or PipeWire system but ended up just picking PipeWire. Most of the issues I've seen with PipeWire seem to stem from bodged migrations from Pulse systems anyway and that is not an issue I would have to deal with. Unfortunately it turns out that for my nice, clean and modern PipeWire system I would still have to build PulseAudio first. While PipeWire provides the compatability layer with applications that use PulseAudio as audio backend (like Firefox) I still need to provide those applications with libpulse first at build time. </p> <p>My package of PulseAudio only provides the library and not the daemon so I have the least amount of conflicting sound systems as possible in the distribution. Of course Alsa is also packaged but that is required anyway by PipeWire to access the hardware. For the Jack parts of PipeWire I have simply decided to not have the Jack parts.</p> <h2>Building Firefox</h2> <p>So once I had all the dependencies out of the way I started the painful process of building Firefox. This part contained a lot of failed builds and many many many hours of wasted build time due to the build failing at the finish line.</p> <p>I spend a lot of time trying to figure out why I had syntax errors in the Rust files for the CSS engine. For some reason the CSS engine for Firefox (The component is called "Style" so it's absolutely impossible to find anything for it in a search engine) has massive <code>.rs</code> files generated by Jinja2 templates and somewhere it's not providing syntaxically valid Rust files anymore. I spend weeks swapping out various dependency versions of various parts to figure out why these files are invalid until I gave up.</p> <p>Then a week later I decided to try again but this time Firefox 134.0 was released so I changed the package to use the newer release, this instantly fixed all the Rust related build issues... Of course I still had a few more build issues to figure out but in the end I was presented with this nice message from the Firefox build system:</p> <figure class="kg-card kg-image-card kg-width-wide"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1737753795/image.png" class="kg-image"></figure> <p>This is probably referring to the 68 minute build time, but I'd like to think the Firefox build system is self aware and knows of my days of debugging.</p> <h2>Increasing the difficulty and building for ARM</h2> <p>So at this point I felt like things were going too easy, so one day I decided it would be funny to rebuild BodgeOS for ARMv8. Due to the way BodgeOS is built this means building the distribution from LFS again since I have no support for cross-building in this system, simply because that's a lot of complication to maintain in the build system for something that is only done very rarely.</p> <p>To build the ARM version of BodgeOS I wanted to build it natively on an ARM system and it turns out that for some reason I have a lot of random ARM systems around :D, I decided to grab the LX2K board I have since it's probably the fastest ARM64 system I have here (I haven't really checked the benchmarks how it compares with the RK3588 systems) but it most certainly is the most sane one of all of them. This is the only ARM SystemReady certified hardware I have, I don't think I can explain it any better than the marketing blurb on the manufacturers website:</p> <figure class="kg-card kg-image-card kg-width-wide"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1737754376/image.png" class="kg-image"><figcaption>The SystemReady ES description from solid-run.com</figcaption></figure> <p>This means I could just unpack the generic Alpine Linux ARM64 rootfs on a random 2.5" SSD I had around, plug it in and boot it. Even easier is that I already had done that before and the SSD I used last time was still in there so I continued on directly with building from the Alpine 3.15 system I had on there.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1737804779/20250125_0005.jpg" class="kg-image"><figcaption>The LX2K board in a 2U rack case powered by a picoPSU</figcaption></figure> <p>From here on out the steps are incredibly similar to bringing up the x86_64 version of BodgeOS. I used the automated JHALFS system to get my clean LFS system to start the whole system from, surprisingly building this on ARM worked practically perfectly the first time while nothing in JHALFS suggests it even is tested on ARM systems.</p> <p>From there on I copied my temporary helper scripts from my LFS installation on my Thinkpad that I used to build the first iteration of x86_64 BodgeOS and hacked together abuild inside LFS to run the build scripts. The rest was actually a lot easier since I didn't have to figure out any of the packaging again, 99% of the package builds scripts simply worked on ARM64 or were simply missing some build dependency declarations.</p> <p>What also suprised me is that this ARM board actually outperformed my relatively modern Thinkpad in build speed. The Thinkpad I've been using for this is an X280 with the i5-8250U CPU in it. What also helps for a few builds is that the LX2K board has twice amount of RAM in it currently than the Thinkpad has (16GB vs 8GB) which means I didn't have to pass in -j4 for a few builds that were very memory heavy. Running with only 4 threads is quite painful on this board anyway because it's speed comes from having 16 cores in it, and the cooling to actually run that at full speed.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1737755007/image.png" class="kg-image"><figcaption>The initial LFS build on the LX2K board</figcaption></figure> <h2>Continuing on</h2> <p>I have gotten to a minimal working installation on the LX2K for the ARM64 build and I've been packaging random tools I need on the x86_64 version now. To continue I should probably make an actual build system that does automated builds from the git repositories on both platforms instead of me just manually triggering abuild for every package and rsync'ing the file over to the mirror.</p> <p>One of my goals other than Firefox was building Kicad but this is more painful than getting Firefox running due to the list of dependencies it requires. The main one being that I actually going to have to build Xorg to build Kicad.</p> BodgeOS pt.3: Graphical desktophttps://blog.brixit.nl/bodgeos-pt-3-graphical-desktop/108LinuxMartijn BraamSun, 29 Dec 2024 20:11:18 -0000<p>In the <a href="https://blog.brixit.nl/bodgeos-pt-2-running-on-real-hardware/">previous post</a> I figured out all the internal weirdness of Linux booting to get BodgeOS running on actual hardware. The next goal was very clear: getting to a graphical environment. At the start of this month I had the goal set to running a web browser before 2024 ends but I've now slightly adjusted my goals down to being able to type this blog post in a terminal on my new OS.</p> <p>So what does it take to get graphics in Linux? well the first component is very clear from experience: Mesa. This is the component that provides all the userspace components for the graphics drivers. I started with checking out both the LFS mesa build instructions and the Alpine and ArchLinux mesa packages. This is not a very nice package to build due to the large dependencies it has. This one project contains all the graphical hardware related code for any GPU Linux will run on and due to that it depends on several programming languages and compilers.</p> <p>I have tried stripping down this package as much as possible: no X11 support, only intel graphics, only EGL for 3D acceleration, no extra components, no software rasterisation. This makes Mesa relatively easy to build. With only the i915 gallium driver for intel graphics I don't have to bring up any of the Vulkan, rust or llvm dependencies to get basic graphics.</p> <h2>The desktop</h2> <p>So I had to pick a graphical environment as goal to run. There are many choices for this and even more opinions on what the best one. I picked Sway here since it's a Wayland based environment so I don't have to go figure out all the X11 stuff. It's also very simple to build compared to something like Gnome or KDE Plasma. I guess there will be someone that has figured out that some random ancient window manager can be built with even less dependencies but this is the smallest one from the desktops I've actually used before :)</p> <p>The dependency chain for Sway is pretty simple: wlroots abstracts away all the Wayland stuffs and makes it actually communicate with Mesa. Then it has some extra dependencies to render text and simple graphics to draw the bars and window decorations.</p> <p>So I started figuring out the minimum dependencies for every component in the dependencies of wlroots and Sway and package all the things are that are needed. This included fixing up the Python udev bindings from my systemd package, packaging the Wayland protocols, a bunch of Xorg keyboard stuff because it seems like keymaps are still used from x* packages and finally seatd to provide a way to get a session for the desktop.</p> <h2>Sway</h2> <p>For Sway the dependencies got a lot more annoying to compile since it depends on Pango and Cairo for rendering and those build systems were just a massive pain to deal with. It seems like the higher you get in the stack of a Linux system the more bullshit is added to build systems to make things "easier". My particiular painpoint in the Sway dependencies is glib and gobject-introspection which is not sufficiently documented and seems to work on magic.</p> <h2>Font rendering</h2> <p>Along with Pango I also had to bring up the whole font system in Linux. This involves Pango, Cairo, HarfBuzz, Glib and several obscure libraries for font processing. These packages are fun because they contain circular dependencies so I had to build them a few extra times.</p> <h2>First attempt at booting</h2> <p>After getting the 41 packages built that are required to get to a very minimal Sway experience I generated a new rootfs and tried booting it on a laptop.</p> <p>This started the hunt for optional dependencies that were not optional for my usecase. The first one was that seatd could not actually make a session for me because I had zero backends compiled in. This was a relatively simple fix of just enabling the builtin backend in seatd to get at least <i>some</i> session.</p> <p>Next came the graphical stack issues...</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1735499877/image.png" class="kg-image"></figure> <p>It first took a bit of time figuring out which of these errors was the fatal one I should be looking at, it was not one of the red ones in this case. The important error here was the "iris: driver missing" line which is from Mesa. I had initially assumed that <code>i915</code> was the hardware backend I needed for my laptops since it's the name I've always come across. Apparently my laptops are old but not old enough to require i915 graphics, instead I needed to enable the <code>iris</code> backend.</p> <p>Enabling the flag for iris in Mesa is very simple, but the hard part is the additional dependencies this adds to Mesa. Iris requires libclc which is the library for OpenCL. This depends on SPIR-V and LLVM which means packaging another massive project.</p> <p>LLVM was by far the biggest time sink for a single package I've had so far. This package takes absolutely forever to compile and I was building this on an x280 with 8GB ram. Since this laptop has 8 cores I have built everything with -j8 so far which works fine except for LLVM where I had to drop to -j4 to not run out of memory while building. I had the same issues with Clang as well and together I've spend 3 days waiting on either one of them to build to hit the next issue that needed slight adjustments in the flags.</p> <p>With LLVM working I managed to build all the packages required for SPIR-V and libclc so I could finally build the iris backend in Mesa. Since I now had a few extra dependencies packaged I also could enable llvmpipe as software rasterizer and osmesa, the off-screen mesa renderer.</p> <h2>Sway starts</h2> <p>With my graphics drivers fixed I finally got Sway to run. This was a very unexciting start though since the only thing it actually rendered was a black screen with my cursor. To make this more annoying to debug it also did not allow me to switch back to a TTY with ctrl+alt+F{1,2,3,4} anymore to see any of the debug logs. This forced me to build the thing I had been postponing: openSSH.</p> <p>By launching Sway through an SSH session I noticed that the first thing I was missing was the <code>swaybg</code> binary which apparently is a seperate package, that explains why by background was completely black. This was packaged and built in a few minutes which fixed 90% of the screen area. The next mystery was the missing bars.</p> <p>Suprisingly with all the logging turned to max in Sway I still got no error message whatsoever about the bars not showing up. Even more suprising is that if I reloaded the config a few times there occiasionally were some graphical artifacts where the bars were supposed to be.</p> <p>After trying a few things and guessing even more things I figured out why it did not show up: I have the entire font rendering pipeline working but I haven't packaged a single font.... So that was an easy fix.</p> <p>To complete the minimal working environment I also built the <code>foot</code> package to have a terminal available that did not have too many extra dependencies to work.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1735502281/11e62db7c0cfe009.jpg" class="kg-image"><figcaption>BodgeOS Sway!</figcaption></figure> <p>There's also several more low-level things I had to figure out on the way, like my installed system not having any locales available. This was quickly fixed by importing the locale-gen script from ArchLinux to generate the locales I need and fixing up my glibc package to put the locale files in the right location.</p> <h2>Branding</h2> <p>So now I have the bare minimum I could focus on more cleanup work and small features. One of those is making the default wallpaper for my distro. I ended up doing the same thing I always do when I get annoyed with Inkscape not doing what I want: Rendering the graphics directly using Python instead.</p> <p>I made a small python script that uses Pillow to render a wallpaper at the requested resolution.</p> <figure class="kg-card kg-image-card kg-width-wide"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1735502591/bodge-crop.png" class="kg-image"></figure> <p>This was inspired by the KiCad PCB editor I had open. I thought it was topical since the distro name was also inspired by my electronics projects :D</p> <h2>Continuing on</h2> <p>So this most likely completes my BodgeOS project for this year. I'm now up to 238 APKBUILD scripts in the repository which build ~900 packages for the distribution.</p> <p>I'll have to package a lot more probably for my next goal, which is getting Firefox to run. This includes some things I've been avoiding like figuring out how to bring up the rust ecosystem and packaging GTK. While the current packages might've been hard to figure out, the rust ecosystem seem to actively resist packaging efforts to make it even harder. Maybe I should get more of Python packaged first so I can use my own utilities for working with APKBUILD files.</p> <p>Since this is also the last blog post of the year, happy new year everyone!</p> Megapixels 2.0 alpha releasehttps://blog.brixit.nl/megapixels-2-0-alpha-release/107MegapixelsMartijn BraamTue, 24 Dec 2024 13:16:57 -0000<p>It's been quite a while since I wrote a Megapixels update post. Since my last post libmegapixels has had a lot more testing on hardware other than the PINE64 devices and the Librem 5 which I originally wrote it for. This obviously found a few flaws in my library code for edge cases I hadn't had to deal with before but overall the fundamental ideas behind the library seem to work.</p> <p>I have now removed the last device-specific workaround from the libmegapixels code and the device support is now purely config files with a few flags to turn on quirks present in a few drivers like not having ioctls implemented correctly.</p> <p>I once again stood before the software release dilemma: should I push a release that's not perfect or keep waiting and waiting to release until every last bug has been ironed out. Currently when running Megapixels 2.0 on the original PinePhone it's not a perfect drop-in replacement with all the features which is why I wanted to hold off on a release. But there's a few other devices that now already have 100% camera functionality on the development branch and for those devices a release would be great.</p> <h2>Megapixels 2.0.0-alpha1</h2> <p>As a compromise I have tagged an alpha release now from the development branch. This was issues can be ironed out that will happen when running Megapixels on one of the many combinations of distributions and devices. Since Megapixels now is also split up in the <code>megapixels</code>, <code>libdng</code> and <code>libmegapixels</code> projects the packaging can also now be figured out. The two libraries also have a new <code>0.2.0</code> tag now that marks the minimum version required for running the alpha release.</p> <p>With this release it also means that all the library apis are now somewhat stable, but more importantly I'm now pretty confident that the config file format won't need any intrusive changes anymore so files for other devices can now be created without risking a lot of breakage down the line.</p> <p>This format now also finally has some proper documentation over at <a href="https://libme.gapixels.me/config.html">https://libme.gapixels.me/config.html</a> because "copy another file and hope for the best" is simply not a great developer experience.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1735046040/IMG_20241224_010735.jpg" class="kg-image"><figcaption>Megapixels 2.0 running on the Samsung Galaxy SIII (ported by @jack_kekzoz)</figcaption></figure> <h2>Many thanks</h2> <p>I've not build this release alone ofcourse. I'd like to thank @k.vos, @pavelm, @pastalian, @jack_kekzoz, @barni2000 and @Luigi311 for their contributions to all the various parts of this codebase. I'd also like to thank the people that have supported my patreon/liberapay to sponsor me working on this :)</p> <h2>The release</h2> <p>The most important link ofcourse, the Megapixels tag:</p> <p><a href="https://gitlab.com/megapixels-org/Megapixels/-/tags/2.0.0-alpha1">https://gitlab.com/megapixels-org/Megapixels/-/tags/2.0.0-alpha1</a></p> <p>The libraries releases are:</p> <ul><li><a href="https://gitlab.com/megapixels-org/libmegapixels/-/tags/0.2.0">https://gitlab.com/megapixels-org/libmegapixels/-/tags/0.2.0</a></li> <li><a href="https://gitlab.com/megapixels-org/libdng/-/tags/0.2.0">https://gitlab.com/megapixels-org/libdng/-/tags/0.2.0</a></li> </ul> <p>Documentation available at:</p> <ul><li><a href="https://libme.gapixels.me/index.html">https://libme.gapixels.me/index.html</a></li> <li><a href="https://libdng.me.gapixels.me/">https://libdng.me.gapixels.me/</a></li> </ul> BodgeOS pt.2: Running on real hardwarehttps://blog.brixit.nl/bodgeos-pt-2-running-on-real-hardware/106LinuxMartijn BraamTue, 17 Dec 2024 00:30:04 -0000<p>In the <a href="https://blog.brixit.nl/conjuring-a-linux-distribution-out-of-thin-air/">previous part</a> of this series I created a base Linux distribution from a running LFS system. This version only ran as a container which has several benefits that makes building the distribution a lot easier. For a simple container I didn't have to have:</p> <ul><li>A service manager (systemd)</li> <li>Something to make it bootable on x86_64 systems (grub, syslinux, systemd-boot)</li> <li>A kernel</li> <li>An initramfs to get my filesystem mounted</li> <li>File-system utilities since there&#x27;s a folder instead of a filesystem.</li> </ul> <p>A few of these are pretty easy to get running. I already have all the dependencies to build a kernel so I generated a kernel from the linux-lts package in Alpine Linux.</p> <p>To make things easier for myself I just limited the distribution to run on UEFI x86_64 systems for now. This means I don't have to mess with grub ever again and I can just dump systemd-boot into my /boot folder to get a functional system. I had to build this anyway since I had to build systemd to have an init system for my distribution.</p> <h2>The Initramfs</h2> <p>The thing that took by far the longest is messing with the initramfs to make my test system boot. The initramfs generator is certainly one of the parts that have the most distribution-specific "flavor". Everyone invents it's own solution for it like <code>mkinitcpio</code> for ArchLinux and <code>mkinitfs</code> for Alpine and <code>initramfs-tools</code> for Debian as a few examples.</p> <p>I did the only logical thing and reinvented the wheel here. I'm even planning to reinvent it even further! Like the above solutions my current initramfs generator is a collection of shell scripts. The initramfs is a pretty simple system after all: it has to load some kernel modules, find the rootfs, mount it and then execute the init in the real system.</p> <p>For a very minimal system the only required thing is the <code>busybox</code> binary, it provides the shell script interpreter required to run the messy shell script that brings up the system and also provides all the base utilities. Due to my previous experiences with BusyBox modprobe in postmarketOS I decided to also move the real <code>modprobe</code> binary in the initramfs to have things loading correctly. To complete it I also added <code>blkid</code> instead of relying on the BusyBox implementation here to have support for partition labels in udev so no custom partition-label-searching code is required.</p> <p>Getting binaries in the initramfs is super easy. The process for generating an initramfs is:</p> <ol><li>Create an empty working directory</li> <li>Move in the files you need into the working directory from the regular rootfs like <code>/usr/bin/busybox</code> &gt; <code>/tmp/initfs-build/usr/bin/busybox</code></li> <li>Add in a script that functions as pid 1 in the initramfs and starts execution of the whole system</li> <li>Run the <code>cpio</code> command against the <code>/tmp/initfs-build</code> directory to create an archive of this temporary rootfs and run that through <code>gzip</code> to generate <code>initramfs.gz</code></li> </ol> <p>Step 2 is fairly simple since I just need to copy the binaries from the host system, but those binaries also have dependencies that need to be copied to make the executable actually work. Normally this is handled by the <code>lddtree</code> utility but I didn't feel like packaging that. It is a shell script that does a complicated task which is never a good thing and it depends on python and calling various ELF binary debugging utilities.</p> <p>Instead of using <code>lddtree</code> I brought up <a href="https://harelang.org/">Hare</a> on my distribution and wrote a replacement utility for it called <a href="https://git.sr.ht/~martijnbraam/hare-bindeps">bindeps</a>. This is just a single binary that loads the ELF file(s) and spits out the dependencies without calling any other tools. This is significantly faster than the performance overhead of <code>lddtree</code> which was always the slowest part of generating the initramfs for postmarketOS.</p> <p>The output format is also optimized to be easily parse-able in the mkinitfs shellscript.</p> <div class="highlight"><pre><span></span><span class="gp">$ </span>lddtree<span class="w"> </span>/usr/sbin/blkid<span class="w"> </span>/usr/sbin/modprobe<span class="w"> </span> <span class="go">/usr/sbin/blkid (interpreter =&gt; /lib64/ld-linux-x86-64.so.2)</span> <span class="go"> libblkid.so.1 =&gt; /lib/x86_64-linux-gnu/libblkid.so.1</span> <span class="go"> libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6</span> <span class="go">/usr/sbin/modprobe (interpreter =&gt; /lib64/ld-linux-x86-64.so.2)</span> <span class="go"> libzstd.so.1 =&gt; /lib/x86_64-linux-gnu/libzstd.so.1</span> <span class="go"> liblzma.so.5 =&gt; /lib/x86_64-linux-gnu/liblzma.so.5</span> <span class="go"> libcrypto.so.3 =&gt; /lib/x86_64-linux-gnu/libcrypto.so.3</span> <span class="go"> libc.so.6 =&gt; /lib/x86_64-linux-gnu/libc.so.6</span> <span class="gp">$ </span>bindeps<span class="w"> </span>/usr/bin/blkid<span class="w"> </span>/usr/bin/modprobe<span class="w"> </span> <span class="go">/usr/lib/ld-linux-x86-64.so.2</span> <span class="go">/usr/lib/libblkid.so.1.1.0</span> <span class="go">/usr/lib/libc.so.6</span> <span class="go">/usr/lib/libzstd.so.1.5.6</span> <span class="go">/usr/lib/liblzma.so.5.6.3</span> <span class="go">/usr/lib/libz.so.1.3.1</span> <span class="go">/usr/lib/libcrypto.so.3</span> </pre></div> <p>The bindeps utility seems to be roughly 100x faster in the few testcases I've used it in and it outputs in a format that needs no further string-mangling to be used in a shell script. In BodgeOS mkinitfs it's used like this:</p> <div class="highlight"><pre><span></span><span class="nv">binaries</span><span class="o">=</span><span class="s2">&quot;/bin/modprobe /bin/busybox /bin/blkid&quot;</span> <span class="k">for</span><span class="w"> </span>bin<span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="nv">$binaries</span><span class="w"> </span><span class="p">;</span><span class="w"> </span><span class="k">do</span> <span class="w"> </span>install<span class="w"> </span>-Dm0755<span class="w"> </span><span class="nv">$bin</span><span class="w"> </span><span class="nv">$workdir</span>/<span class="nv">$bin</span> <span class="k">done</span> bindeps<span class="w"> </span><span class="nv">$binaries</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="k">while</span><span class="w"> </span><span class="nb">read</span><span class="w"> </span>lib<span class="w"> </span><span class="p">;</span><span class="w"> </span><span class="k">do</span> <span class="w"> </span>install<span class="w"> </span>-Dm0755<span class="w"> </span><span class="nv">$lib</span><span class="w"> </span><span class="nv">$workdir</span>/<span class="nv">$lib</span> <span class="k">done</span> </pre></div> <p>The next part is the kernel modules. Kernel modules are also ELF binaries just like the binaries I just copied over but they sadly don't contain any dependency metadata. This metadata is stored in a seperate file called <code>modules.dep</code> that has to be parsed seperately. I did not bother with this and copied the solution from the initramfs generator example from LFS and just copy hardcoded folders of modules into the initramfs and hope it works.</p> <p>The file format for <code>modules.dep</code> is trivial so I really want to just integrate support for that into bindeps in the future.</p> <h2>Debugging the boot</h2> <p>It's suprisingly painful to debug a non-booting Linux system that fails in the initramfs. I wasted several hours figuring out why the kernel threw errors at the spot the initramfs should start executing which ended up being an issue with the <code>/sbin/init</code> file had the wrong shebang line at the start so it was not loadable. The kernel has no proper error message that conveys any of this.</p> <p>After I got the initramfs to actually load and start a lot of time was wasted on executables missing the interperter module. In the example above this is the <code>/lib64/ld-linux-x86-64.so.2</code> line. The issue here ended up that I was just missing the /lib64 symlink in my initramfs. This was very hard to debug in a system without debug utilities because nothing could execute.</p> <p>After all that I spend even more time figuring out why I had no kernel log lines on my screen. After much annoyance this turned out to be missing options in the kernel config for the linux-lts kernel config I took from Alpine Linux. So instead of fixing that I took the kernel config from ArchLinux and rebuild the linux-lts package. This fixed my kernel log output issue but also added a new one... The keyboard in my laptop wasn't working in the initramfs.</p> <p>I never did figure out which module I was missing for that because I fixed the rest of the initramfs script instead so it just continues on to the real rootfs where all the modules are available.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1734393060/16f5654c63ad395d.jpg" class="kg-image"></figure> <p>After all that I did manage to get to a login prompt though!</p> <h2>Cleaning up</h2> <p>After booting up this I realized it would be really handy if I actually had a <code>/etc/passwd</code> file in my system and some more of the bare essentials so I could actually log in and use the system.</p> <p>This mainly involved adding missing dependencies in packages and packaging a few more files in <code>/etc</code> to make it a functional system. After the first boot the journal had a neat list of system binaries from util-linux that systemd depends on but not explicitly, so I added those dependencies to my systemd packaging.</p> <p>I also had to fix the issue that my newly-installed system did not trust the BodgeOS repository, I was missing the keys package that installs the repository key in <code>/etc/apk/keys</code> for me. In this process I noticed that the key I built the system with was called `-randomdigits.pub` instead of being prefixed with a name. This is pretty annoying because this name is embedded in all the compiled packages and I didn't want to ship a file with that name in my keys package.</p> <p>There seemed to be a nifty solution though: the <code>abuild-sign</code> tool appends a key to a tar archive, which is normally used to sign the APKINDEX.tar.gz file that contains the package list in the repository. I decided to run <code>abuild-sign *.apk</code> in my main repository after adjusting the abuild signing settings with a correct key name.</p> <p>Apparently this breaks .apk files and after inspection they now had two keys in them and neither my development LFS install and my test BodgeOS install wanted to have anything to do with the packages anymore.</p> <p>In the end I had to throw away my built packages and rebuild everything again from the APKBUILD files I had. Luckily this distribution is not that big yet so a full rebuild only took about 2.5x the duration of Dark Side of the Moon.</p> <h2>Next steps</h2> <p>Now I have a basic system that can boot I continued with packaging more libraries and utilites you'd expect in a regular Linux distribution. One of the things I thought would be very neat is having <code>curl</code> available, but that has a suprising amount of dependencies. Some tools like <code>git</code> will be useful too before I can call this system self-hosting.</p> <p>I also want to remove all the shell scripts from the initramfs generation. None of the tasks in the initramfs are really BodgeOS specific and most of the complications and bugs in this initramfs implementation (and the one in postmarketOS) is because the utilities it depends on are not really intended to do this stuff and system bootup just has a lot of race conditions shell scripts are just not great at handling.</p> <p>My current plan to fix that is to just replace the entire initramfs with a single statically linked binary. All this logic is way neater to implement in a good programming language. </p> Conjuring a Linux distribution out of thin airhttps://blog.brixit.nl/conjuring-a-linux-distribution-out-of-thin-air/105LinuxMartijn BraamSat, 07 Dec 2024 23:08:27 -0000<p>I decided I had to get something with slightly more CPU power than my Thinkpad x230 for a few tasks so I got a refurbished x280, aside from the worse keyboard the laptop is pretty nice and light. It shipped with Windows of course so the first thing I did is to install Ubuntu on the thing to wipe that off and verify all the hardware is working decently.</p> <p>I was wondering if I should leave Ubuntu on the thing, it works pretty well and it's still possible to get rid of all the Snap stuff, it's not my main machine anyway. The issue I ran into quickly though is some software is pretty outdated, like I don't want to use Kicad 7 anymore...</p> <h2>Picking distros once again</h2> <p>You'd think after using Linux for decades I would know what distro I'd put on a new machine. All the options I could think off though had annoying trade-offs I didn't want to deal with once again.</p> <p>The three main distributions I have running on hardware I manage is Alpine Linux, Archlinux and Debian. I like Alpine a lot but it is quite annoying when you deal with closed-source software. Since this is my go-to laptop to take with me to outages and repairs then I need it to handle random software thrown on it relatively easily.</p> <p>ArchLinux satisfies that requirement pretty well but my main issue with it is pacman. If you don't religiously run upgrades every hour on the thing it will just break because for some reason key management is not figured out yet there. The installation is also quite big usually due to packages not being split.</p> <p>Debian fixes the stability issues but comes with the trade off that software is usually much much older, this also leaks into Ubuntu that's running on the laptop now. It is also internally a lot more complicated due to the way it automatically sets up stuff while installing which I don't usually need.</p> <p>There is another solution though. Just build my own!</p> <h2>Artisanal home-grown Linux</h2> <p>Creating a new Linux distribution is one of those things that sounds much harder than it actually is. I just haven't done it before. I did build a small Debian derivative distro before just to avoid re-doing all the config for all machines but that's just adding an extra repository to an existing distribution. Of course I've also worked extensively on postmarketOS and while the scope of that is a lot larger it still is only a repository with additions on Alpine Linux.</p> <p>Some of you might be familiar with this graphic of Linux distributions:</p> <figure class="kg-card kg-image-card kg-width-wide"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1733576472/image.png" class="kg-image"></figure> <p>There are many many many derivative distributions here, way less distributions that are built up from nothing. And that's exactly the part that I want to figure out. How do I make a distribution from scratch?</p> <p>I have once, a long time ago, build a working Linux machine from sources using the great <a href="https://www.linuxfromscratch.org/">Linux From Scratch</a> project. I would recommend that anyone that's really into Linux internals do that at least once. Just like Archlinux learns you how the distribution installer works (before they added an installer) the LFS book will learn you how you bootstrap a separate userspace from another Linux distribution and doing the gcc/glibc dependency loop build.</p> <p>So my plan for the distribution is: create a super barebones system using the LFS book, package up that installation using abuild and apk from Alpine Linux. This way I can basically make my own systemd/glibc distribution that is mostly like Archlinux but uses the packaging tools and methodology from Alpine Linux.</p> <h2>The bootstrap build</h2> <p>So to make my distribution I first have to build the Linux installation that will build the packages for the distribution. To get through this part relatively quickly I used the automated LFS installer called <a href="https://www.linuxfromscratch.org/alfs/">ALFS</a>. This basically does all the steps from the LFS book but very quickly. My intention is to replace all of these packages anyway so this part was not super important. It does all the required setup though to validate the GCC I'm using to build my distribution is sane and tested.</p> <p>In the ALFS installer I picked the systemd option since I didn't want to deal with openrc again and ended up with a nice functional rootfs to work in. I immediately encountered a few things that were critical and missing though. There was no wget or curl. I fixed this by grabbing the Busybox binary from my host Ubuntu distribution and putting that inside with only a wget symlink to it.</p> <p>This wget was not functional in my chroot though since it could not connect to https servers. Annoyingly all solutions you'd find for these errors is passsing <code>--no-check-certificates</code> to wget to get your files which is unacceptable when building a distribution. After a lot of debugging with openssl configs I ended up copying over all the certificates from the host Ubuntu system again and pointed wget to the certificate bundle with the wgetrc file.</p> <p>The very next thing I needed is <code>abuild</code>. This is the tool from Alpine Linux that's used to build the packages. Luckily it's a few small C programs and a large shell script so it's very easy to install in the temporary system. I also added <code>apk.static</code> to the system to be able to install the built packages.</p> <h2>Building packages</h2> <p>So now I have my temporary system running I could start writing <code>APKBUILD</code> files for all the packages in my LFS system. I started with the very simplest one of course that only provides <code>/etc/services</code> and <code>/etc/protocols</code>. No compiling and dependencies needed.</p> <p>For this package made the script that built the current APKBUILD using the abuild subcommands and generated a neat local repository so apk could install the files. So I ran that and now <code>/etc/services</code> and <code>/etc/protocols</code> are now exactly the same files but managed by apk and in a package.</p> <p>The reason I had to use the subcommands for abuild to run seperate stages instead of just running abuild itself to do the whole things is because one of the first steps is installing the build dependencies. In this case I'm in the weird setup where I have all the build dependencies installed through LFS but apk doesn't know about that so I simply skip that step.</p> <p>And that's how I re-build a lot of the LFS packages once again, but this time through my half broken abuild installation. One of the things that abuild was not happy about is the lack of the <code>scanelf</code> utility which it uses after building a package to check which <code>.so</code> files the binaries in the package depend on. Due to this a lot of dependencies between packages are simply missing. The <code>scanelf</code> utility has enough build dependencies though that I could not have that as the first few packages so most of the packages are broken in this stage.</p> <p>When I finally built and installed <code>scanelf</code> I ran into another issue. The packages I build after this failed at the last step because scanelf found the .so files required for the package but the package metadata for all the packages I made before it lack this information about the included .so files apparently. At this point I had to build all those packages for a fourth time (twice in LFS and now twice in abuild) to make dependencies here work.</p> <p>After getting through practically everything in the base system I ended up with around 333 packages in my local repository.</p> <p>Most of these <code>APKBUILD</code> files are a combination of the metadata header copied from the Alpine Linux ABUILD so I have a neat description and the correct license data. And then the build steps and flags from LFS and sometimes the install step from Archlinux.</p> <p>This means that for example the <code>xz</code> build steps in LFS are:</p> <div class="highlight"><pre><span></span><span class="gp">$ </span>./configure<span class="w"> </span>--prefix<span class="o">=</span>/usr<span class="w"> </span><span class="se">\</span> <span class="w"> </span>--disable-static<span class="w"> </span><span class="se">\</span> <span class="w"> </span>--docdir<span class="o">=</span>/usr/share/doc/xz-5.6.3 <span class="gp">$ </span>make <span class="gp">$ </span>make<span class="w"> </span>check <span class="gp">$ </span>make<span class="w"> </span>install </pre></div> <p>And that combined with the Alpine Linux metadata headers and adjustments for packaging becomes:</p> <div class="highlight"><pre><span></span><span class="nv">pkgname</span><span class="o">=</span>xz <span class="nv">pkgver</span><span class="o">=</span><span class="m">5</span>.6.3 <span class="nv">pkgrel</span><span class="o">=</span><span class="m">0</span> <span class="nv">pkgdesc</span><span class="o">=</span><span class="s2">&quot;Library and CLI tools for XZ and LZMA compressed files&quot;</span> <span class="nv">url</span><span class="o">=</span><span class="s2">&quot;https://tukaani.org/xz/&quot;</span> <span class="nv">arch</span><span class="o">=</span><span class="s2">&quot;all&quot;</span> <span class="nv">license</span><span class="o">=</span><span class="s2">&quot;GPL-2.0-or-later AND 0BSD AND Public-Domain AND LGPL-2.1-or-later&quot;</span> <span class="nv">subpackages</span><span class="o">=</span><span class="s2">&quot;</span><span class="nv">$pkgname</span><span class="s2">-doc </span><span class="nv">$pkgname</span><span class="s2">-libs </span><span class="nv">$pkgname</span><span class="s2">-dev&quot;</span> <span class="nv">source</span><span class="o">=</span><span class="s2">&quot;https://github.com//tukaani-project/xz/releases/download/v</span><span class="nv">$pkgver</span><span class="s2">/xz-</span><span class="nv">$pkgver</span><span class="s2">.tar.xz&quot;</span> build<span class="o">()</span><span class="w"> </span><span class="o">{</span> <span class="w"> </span>./configure<span class="w"> </span><span class="se">\</span> <span class="w"> </span>--prefix<span class="o">=</span>/usr<span class="w"> </span><span class="se">\</span> <span class="w"> </span>--disable-static<span class="w"> </span><span class="se">\</span> <span class="w"> </span>--docdir<span class="o">=</span>/usr/share/doc/xz-<span class="nv">$pkgver</span> <span class="w"> </span>make <span class="o">}</span> check<span class="o">()</span><span class="w"> </span><span class="o">{</span> <span class="w"> </span>make<span class="w"> </span>check <span class="o">}</span> package<span class="o">()</span><span class="w"> </span><span class="o">{</span> <span class="w"> </span>make<span class="w"> </span><span class="nv">DESTDIR</span><span class="o">=</span><span class="s2">&quot;</span><span class="nv">$pkgdir</span><span class="s2">&quot;</span><span class="w"> </span>install <span class="o">}</span> </pre></div> <h2>Getting the first install to run</h2> <p>Now the hard part, finding all the issues that prevent this new installation from starting. The first thing I tried to use the <code>apk.static</code> on my host system to generate a new chroot from the repository I created, just like you'd install an Alpine chroot.</p> <p>Unfortunately this did not work and I had to fork apk-tools and make my own adjusted version. This is mainly because apk-tools hardcodes paths in it which conflict with my usrmerge setup. So I now have an <code>apk.static</code> build from my fork that does not try to create <code>/var/</code> for the database before the baselayout package can create the actual filesystem hierarchy with a symlink at that spot.</p> <p>With that fixed <code>apk.static</code> would be able to finish creating an installation from my repository, but I could not chroot into it for some reason. All the binaries are broken and return "Not found" when trying to execute them. I managed to actually enter the chroot by throwing my trusty busybox binary in there but did not get any more information out of that installation.</p> <p>After a bunch of testing, debugging, and more testing, I found out the reason was that I don't have <code>/lib64</code> in my installation. It seems like it's required specifically because x86_64 binaries specify /lib64/ld-linux-x86-64.so.2 as loader. The fix for that is quite easy by just having the glibc package place a symlink at that spot to the real ld-linux.so.</p> <h2>Beyond the first run</h2> <p>There's a lot of things that need to be fixed up to be a good distribution. All the packages will need to be rebuild again from the distribution installled from these first generation packages to leave behind the last parts of LFS in there. There's also the system setup that needs to happen to make it bootable and maintainable. For example things like the keys package that install and update the repository keys and adding the logo to neofetch (after packaging neofetch).</p> <p>I've also rsync'd the repository for the distribution to a webserver so it can actually be added to installations. I've been using this now to create test chroots using my locally build patched apk-tools.</p> <figure class="kg-card kg-image-card kg-width-wide"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1733611608/image.png" class="kg-image"></figure> <p>The repository itself also needs quite a bit of work, gcc shouldn't've been pulled in here for these packages and a bunch of the large packages need to be split up to remove the uncommon parts. Currently the <code>glibc</code> package is already 10x larger than a base Alpine Linux installation, luckily apk is a very fast package manager and everything still installs super quickly.</p> <h2>So why?</h2> <p>It doesn't really make sense to do this. I wanted to have done it anyway because that's how you learn. This took about 3 days of fiddling with build scripts between other work since it's mostly waiting on builds to finish.</p> <p>At the very least this distribution created this blog post, which is surprisingly one of the very few pieces of information available for bootstrapping a distribution.</p> <p>Even with this tiny base of packages this is already a quite usable OS since I have a kernel, a service manager and python. All you need to build some embedded stuff if this had an ARM64 port bootstrapped or something. For desktop work there's still a mountain of work to be done to package everything required to launch Firefox, getting the basic graphics stack up and running should be relatively straightforward with bringing up Sway with its dependencies.</p> <p>In the end it probably would've been easier to just add a ppa for Kicad to my Ubuntu installation :)</p> Building a timeseries database for funhttps://blog.brixit.nl/building-a-timeseries-db-for-fun/103LinuxMartijn BraamMon, 28 Oct 2024 22:50:55 -0000<p>Everyone that has tried to make some nice charts in Grafana has probably come across timeseries databases, for example InfluxDB or Prometheus. I've deployed a few instances of these things for various tasks and I've always been annoyed by how they work. But this is a consequence of having great performance right?</p> <p>The thing is... most the dataseries I'm working with don't need that level of performance. If you're just logging the power delivered by a solar inverter to a raspberry pi then you don't need a datastore for 1000 datapoints per second. My experience with timeseries is not that performance is my issue but the queries I want to do which seem very simple are practically impossible, especially when combinated with Grafana.</p> <p>Something like having a daily total of a measurement as a bar graph to have some long-term history with keeping the bars aligned to the day boundary instead of 24 hour offsets based on my current time. Or being able to actually query the data from a single month to get a total instead of querying chunks of 30.5 days.</p> <p>But most importantly, writing software is fun and I want to write something that does this for me. Not everything has to scale to every usecase from a single raspberry pi to a list of fortune 500 company logos on your homepage.</p> <h2>A prototype</h2> <p>Since I don't care about high performance and I want to prototype quickly I started with a Python Flask application. This is mainly because I already wrote a bunch of Python glue before to pump the data from my MQTT broker into InfluxDB or Prometheus so I can just directly integrate that.</p> <p>I decided that as storage backend just using a SQLite database will be fine and to integrate with Grafana I'll just implement the relevant parts of the Prometheus API and query language.</p> <p>To complete it I made a small web UI for configuring and monitoring the whole thing. Mainly to make it easy to adjust the MQTT topic mapping without editing config files and restarting the server.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1730152571/image.png" class="kg-image"></figure> <p>I've honestly probably spend way too much time writing random javascript for the MQTT configuration window. I had already written a MQTT library for Flask that allows using the Flask route syntax to extract data from the topic so I reused that backend. To make that work nicely I also wrote a simple parser for the syntax in Javascript to visualize the parsing while you type and give you dropdowns for selecting the values.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1730152874/image.png" class="kg-image"></figure> <p>This is not at all related to the dataseries part but at least it allows me to easily get a bunch of data into my test environment while writing the rest of the code.</p> <h2>The database</h2> <p>For storing the data I'm using the <code>sqlite3</code> module in Python. I dynamically generate a schema based on the data that's coming in with one table per measurement.</p> <p>There's two kinds of devices on my MQTT broker, some send the data as a JSON blob and some just send single values to various topics.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1730153124/image.png" class="kg-image"><figcaption>Example data from the MQTT broker</figcaption></figure> <p>The JSON blobs are still considered a single measurement and all the top-level values get stored in seperate columns. Later in the querying stage the specific column is selected.</p> <p>My worst case is a bunch of ESP devices that measure various unrelated things and output JSON to the topic shown above with JSON. I have a single ingestion rule in my database that grabs <code>devices/hoofdweg/<sensor></code> and dumps it in a table that has the columns for the various sensors, which ends up with a schema like this:</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1730153341/image.png" class="kg-image"></figure> <p>A timestamp is stored, no consideration is made for timezones since in practically all cases a house isn't located right on a timezone boundary. The tags are stored in seperate columns with a <code>tag_</code> prefix and the fields are stored in column with a <code>field_</code> prefix. The maximum granularity of data is also a single second since I don't store the timestamp as a float.</p> <p>A lot of the queries I do don't need every single datapoint though but instead I just need hourly, daily or monthly data. For that a second table is created with the same structure but with aggregated data:</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1730153646/image.png" class="kg-image"></figure> <p>This contains a row for every hour with the <code>min()</code>, <code>max()</code> and <code>avg()</code> of every field, it also contains a row for every day and one for every month. This makes it possible to after a preconfigured amount of time just throw away the data that has single-second granularity and keep the aggregated data way longer. For querying you explicitly select which table you want the data from.</p> <h2>The querying</h2> <p>To make the Grafana UI not complain too much I kept the language syntax the same as Prometheus but simply implemented less of the features because I don't use most of them. The supported features right now are:</p> <ul><li>Simple math queries like <code>1+1</code>, this can only do addition queries and is only here to satisfy the Grafana connection tester.</li> <li>Selecting a single measurement from the database and filtering on tags using the braces like <code>my_sensors{sensor=&quot;solar&quot;}</code></li> <li>Selecting a time granularity with brackets like <code>example_sensor[1h]</code>. This only supports <code>1h</code>, <code>1d</code> and <code>1M</code> and selects which rows are queried</li> <li>The aggregate functions like <code>max(my_sensors[1h])</code> which makes it select the columns from the reduced table with the <code>max_</code> prefix for querying when using the reduced table. For selecting the realtime data it will use the SQLite <code>max()</code> function.</li> </ul> <p>This is also just about enough to make the graphical query builder in Grafana work for most cases. The other setting used for the queries is the <code>step</code> value that Grafana calculates and passes to the Prometheus API. For the reduced table this is completely ignored and for the realtime table this is converted to SQL to do aggregation across rows.</p> <p>As an example the query <code>avg(sensors{sensor="solar", _col="voltage"})</code> gets translated to:</p> <div class="highlight"><pre><span></span><span class="k">SELECT</span> <span class="w"> </span><span class="n">instant</span><span class="p">,</span> <span class="w"> </span><span class="n">tag_sensor</span><span class="p">,</span> <span class="w"> </span><span class="k">avg</span><span class="p">(</span><span class="n">field_voltage</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">field_voltage</span> <span class="k">FROM</span><span class="w"> </span><span class="n">series_sensors</span> <span class="k">WHERE</span><span class="w"> </span><span class="n">instant</span><span class="w"> </span><span class="k">BETWEEN</span><span class="w"> </span><span class="o">?</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="o">?</span><span class="w"> </span><span class="c1">-- Grafana time range</span> <span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">tag_sensor</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">?</span><span class="w"> </span><span class="c1">-- solar</span> <span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">instant</span><span class="o">/</span><span class="mi">30</span><span class="w"> </span><span class="c1">-- 30 is the step value from Grafana</span> </pre></div> <p>To get nice aligned hourly data for a bar chart the query simply changes to <code>avg(sensors{sensor="solar", _col="voltage"}[1h])</code> which generates this query:</p> <pre><code>SELECT instant, date, hour, tag_sensor, avg_voltage FROM reduced_sensors WHERE instant BETWEEN ? AND ? -- Grafana time range AND tag_sensor = ? -- solar AND scale = 0 -- hourly</code></pre> <p>This reduced data is generated as background task in the server and makes sure that the row with the aggregate of a single hour selects the datapoints that fit exactly in that hour, not shifted by the local time when querying like I now have issues with in Grafana:</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1730154759/image.png" class="kg-image"><figcaption>The query running against the old Prometheus database</figcaption></figure> <p>The bars in this chart don't align with the dates because this screenshot wasn't made at midnight. The data in the bars is also only technically correct when viewing the Grafana dashboard at midnight since on other hours it selects data from other days as well. If I view this at 13:00 then I get the data from 13:00 the day before to today which is a bit annoying in most cases and useless in the case of this chart because the <code>daily_total</code> metric in my solar inverter is reset at night and I pick the highest value.</p> <p>For monthly bars this issue gets worse because it's apparently impossible to accurately get monthly data from the timeseries databases I've used. Because I'm pregenerating this data instead of using magic intervals this also Just Works(tm) in my implementation.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1730155171/image.png" class="kg-image"><figcaption>The same sort of query on the Miniseries backend, hourly instead because I don&#x27;t have enough demo data yet.</figcaption></figure> <h2>Is this better?</h2> <p>It is certainly in the prototype stage and has not had enough testing to find weird edgecases. It does provide all the features though I need to recreate by existing home automation dashboard and performance is absolutely fine. The next step here is to implement a feature to lie to Grafana about the date of the data to actually use the heatmap chart to show data from multiple days as multiple rows.</p> <p>Once the kinks are worked out in this prototype it's probably a good idea to rewrite it into something like Go for example because while a lot of the data processing is done in SQLite the first bottleneck will probably be the single-threaded nature of the webserver and the MQTT ingestion code.</p> <p>The source code is online at <a href="https://git.sr.ht/~martijnbraam/miniseries">https://git.sr.ht/~martijnbraam/miniseries</a></p> Making a Linux-managed network switchhttps://blog.brixit.nl/making-a-linux-managed-network-switch/102LinuxMartijn BraamWed, 03 Jul 2024 14:10:04 -0000<p>Network switches are simple devices, packets go in, packets go out. Luckily people have figured out how to make it complicated instead and invented managed switches.</p> <p>Usually this is done by adding a web-interface for configuring the settings and see things like port status. If you have more expensive switches then you'd even get access to some alternate interfaces like telnet and serial console ports.</p> <p>There is a whole second category of managed switches though that people don't initially think of. These are the network switches that are inside consumer routers. These routers are little Linux devices that have a switch chip inside of them, one or more ports are internally connected to the CPU and the rest are on the outside as physical ports.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1719959978/RB2011UiAS-160620170256_160656.png" class="kg-image"><figcaption>Mikrotik RB2011 block diagram from mikrotik.com</figcaption></figure> <p>Here is an example of such a device that actually has this documented. I always thought that the configuration of these switch connected ports was just a nice abstraction by the webinterface but I was suprised to learn that with the DSA and switchdev subsystem in Linux these ports are actually fully functioning "local" network ports. Due to this practically only being available inside integrated routers It's pretty hard to play around with unfortunately.</p> <p>What is shown as a single line on this diagram is actually the connection of the SoC of the router and the switch over the SGMII bus (or maybe RGMII in this case) and a management bus that's either SMI or MDIO. Network switches have a lot of these fun acronyms that even with the full name written out make little sense unless you know how all of this fits together.</p> <p>Controlling your standard off-the-shelf switch using this system simply isn't possible because the required connections of the switch chip aren't exposed for this. So there's only one option left...</p> <h2>Making my own gigabit network switch</h2> <p>Making my own network switch can't be <i>that</i> hard right? Those things are available for the price of a cup of coffee and are most likely highly integrated to reach that price point. Since I don't see any homemade switches around on the internet I guess the chips for those must be pretty hard to get...</p> <figure class="kg-card kg-image-card kg-width-wide"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1719960715/image.png" class="kg-image"></figure> <p>Nope, very easy to get. There's even a datasheet available for these. So I created a new KiCad project and started creating some footprints and symbols.</p> <p>I'm glad there's any amount of datasheet available for this chip since that's not usually the case for Realtek devices, but it's still pretty minimal. I resorted to finding any devices that has schematics available for similar Realtek chips to find out how to integrate it and looking at a lot of documentation for how to implement ethernet in a design at all.</p> <p>The implementation for the chip initially looked very complicated, there's about 7 different power nets it requires and there are several pretty badly documented communication interfaces. After going through other implementations it seem like the easiest way to power it is just connect all the nets with overlapping voltage ranges together and you're left with only needing a 3.3V and 1.1V regulator.</p> <p>The extra communication busses are for all the extra ports I don't seem to need. The switch chip I selected is the RTL8367S which is a very widely used 5-port gigabit switch chip, but it's actually not a 5-port chip. It's a 7 port switch chip where 5 ports have an integrated PHY and two are for CPU connections.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1719961532/image.png" class="kg-image"><figcaption>CPU connection block diagram from the RTL8367S datasheet</figcaption></figure> <p>My plan is different though, while there are these CPU ports available there is actually nothing in the Linux switchdev subsystem that requires the CPU connection to be to those ports. Instead I'll be connecting to port 0 on the switch with a network cable and as far as the switchdev driver knows there's no ethernet PHY in between.</p> <p>The next hurdle is the configuration of the switch chip, there's several configuration systems available and the datasheet does not really describe what is the minimum required setup to actually get it to function as a regular dumb switch. To sum up the configuration options of the chip:</p> <ul><li>There&#x27;s 8 pins on the chip that are read when it&#x27;s starting up. These pins are shared with the led pins for the ports so that makes designing pretty annoying. Switching the setting from pull-up to pull-down also requires the led to be connected in the different orientation.</li> <li>There&#x27;s an i2c bus that can be connected to an eeprom chip. The pins for this are shared with the SMI bus that I require to make this chip talk to Linux though. There is pin configuration to select from one of two eeprom size ranges but does not further specify what this setting actually changes.</li> <li>There&#x27;s a SPI bus that supports connecting a NOR flash chip to it. This can store either configuration registers or firmware for the embedded 8051 core depending on the configuration of the bootup pins. The SPI bus pins are also shared with one of the CPU network ports.</li> <li>There is a serial port available but from what I guess it probably does nothing at all unless there&#x27;s firmware loaded in the 8051.</li> </ul> <p>My solution to figuring out is to just order a board and solder connections differently until it works. I've added a footprint for a flash chip that I ended up not needing and for all the configuration pins I added solder jumpers. I left out all the leds since making that configurable would be pretty hard.</p> <p>The next step is figuring out how to do ethernet properly. There has been a lot of documentation written about this and they all make it sound like gigabit ethernet requires perfect precision engineering, impedance managed boards and a blessing from the ethernet gods themselves to work. This does not seem to match up with the reality that these switches are very very cheaply constructed and seem to work just fine. So I decided to open up a switch to check how many of these coupling capacitors and impedance matching planes are actually used in a real design. The answer seems to be that it doesn't matter that much.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1719962591/image.png" class="kg-image"></figure> <p>This is the design I have ended up with now but it is not what is on my test PCB. I got it almost right the first time though :D</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1719962813/image.png" class="kg-image"></figure> <p>The important parts seem to be matching the pair skew but matching the length of the 4 network pairs is completely useless, this is mainly because network cables don't have the same twisting rate for the 4 pairs and so the length of these are already significantly different inside the cable.</p> <p>The pairs between the transformer and the RJ45 jack has it's own ground plane that's coupled to the main ground through a capacitor. The pairs after the transformer are just on the main board ground fill.</p> <p>What I did wrong on my initial board revision was forgetting the capacitor that connects the center taps of the transformer on the switch side to ground making the signals on that side referenced to board ground. This makes ethernet very much not work anymore so I had to manually cut tiny traces on the board to disconnect that short to ground. In my test setup the capacitor just doesn't exist and all the center taps float. This seems to work just fine but the final design does have that capacitor added.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1720003020/fixed.JPG" class="kg-image"><figcaption>Cut ground traces on the ethernet transformer</figcaption></figure> <p>The end result is this slightly weird gigabit switch. It has 4 ports facing one direction and one facing backwards and it is powered over a 2.54mm pinheader. I have also added a footprint for a USB Type-C connector to have an easy way to power it without bringing out the DuPont wires.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1720007603/IMG_20240626_221246.jpg" class="kg-image"></figure> <h2>Connecting it to Linux</h2> <p>For my test setup I've picked the PINE64 A64-lts board since it has the connectors roughly in the spots where I want them. It not being an x86 platform is also pretty important because configuration requires a device tree change, can't do that on a platform that doesn't use device trees.</p> <p>The first required thing was rebuilding the kernel for the board since most kernels simply don't have these kernel modules enabled. For this I enabled these options:</p> <ul><li><code>CONFIG_NET_DSA</code> for the Distributed Switch Architecture system</li> <li><code>CONFIG_NET_DSA_TAG_RTL8_4</code> for having port tagging for this Realtek switch chip</li> <li><code>CONFIG_NET_SWITCHDEV</code> the driver system for network switches</li> <li><code>CONFIG_NET_DSA_REALTEK</code>, <code>CONFIG_NET_DSA_REALTEK_SMI</code>, <code>CONFIG_NET_DSA_REALTEK_RTL8365MB</code> for the actual switch chip driver</li> </ul> <p>Then the more complicated part was figuring out how to actually get this all loaded. In theory it is possible to create a device tree overlay for this and get it loaded by U-Boot. I decided to not do that and patch the device tree for the A64-lts board instead since I'm rebuilding the kernel anyway. The device tree change I ended up with is this:</p> <pre><code>diff --git a/arch/arm64/boot/dts/allwinner/sun50i-a64-pine64-lts.dts b/arch/arm64/boot/dts/allwinner/sun50i-a64-pine64-lts.dts index 596a25907..10c1a5187 100644 --- a/arch/arm64/boot/dts/allwinner/sun50i-a64-pine64-lts.dts +++ b/arch/arm64/boot/dts/allwinner/sun50i-a64-pine64-lts.dts @@ -18,8 +18,78 @@ led { gpios = &lt;&amp;r_pio 0 7 GPIO_ACTIVE_LOW&gt;; /* PL7 */ }; }; + +switch { + compatible = &quot;realtek,rtl8365rb&quot;; + mdc-gpios = &lt;&amp;pio 2 5 GPIO_ACTIVE_HIGH&gt;; // PC5 + mdio-gpios = &lt;&amp;pio 2 7 GPIO_ACTIVE_HIGH&gt;; // PC7 + reset-gpios = &lt;&amp;pio 8 5 GPIO_ACTIVE_LOW&gt;; // PH5 + realtek,disable-leds; + + mdio { + compatible = &quot;realtek,smi-mdio&quot;; + #address-cells = &lt;1&gt;; + #size-cells = &lt;0&gt;; + + ethphy0: ethernet-phy@0 { + reg = &lt;0&gt;; + }; + + ethphy1: ethernet-phy@1 { + reg = &lt;1&gt;; + }; + + ethphy2: ethernet-phy@2 { + reg = &lt;2&gt;; + }; + + ethphy3: ethernet-phy@3 { + reg = &lt;3&gt;; + }; + + ethphy4: ethernet-phy@4 { + reg = &lt;4&gt;; + }; + }; + + ports { + #address-cells = &lt;1&gt;; + #size-cells = &lt;0&gt;; + + port@0 { + reg = &lt;0&gt;; + label = &quot;cpu&quot;; + ethernet = &lt;&amp;emac&gt;; + }; + + port@1 { + reg = &lt;1&gt;; + label = &quot;lan1&quot;; + phy-handle = &lt;&amp;ethphy1&gt;; + }; + + port@2 { + reg = &lt;2&gt;; + label = &quot;lan2&quot;; + phy-handle = &lt;&amp;ethphy2&gt;; + }; + + port@3 { + reg = &lt;3&gt;; + label = &quot;lan3&quot;; + phy-handle = &lt;&amp;ethphy3&gt;; + }; + + port@4 { + reg = &lt;4&gt;; + label = &quot;lan4&quot;; + phy-handle = &lt;&amp;ethphy4&gt;; + }; + }; +}; }; </code></pre> <p>It loads the driver for the switch with the <code>realtek,rtl8365rb</code>, this driver supports a whole range of Realtek switch chips including the RTL8367S I've used in this design. I've removed the CPU ports from the documentation example and just added the definitions of the 5 regular switch ports.</p> <p>The important part is in <code>port@0</code>, this is the port that is facing backwards on my switch and is connected to the A64-lts, I've linked it up to <code>&emac</code> which is a reference to the ethernet port of the computer. The rest of the ports are linked up to their respective PHYs in the switch chip. </p> <p>In the top of the code there's also 3 GPIOs defined, these link up to SDA/SCL and Reset on the switch PCB to make the communication work. After booting up the system the result is this:</p> <pre><code>1: lo: &lt;LOOPBACK&gt; mtu 65536 qdisc noop state DOWN qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: eth0: &lt;BROADCAST,MULTICAST&gt; mtu 1508 qdisc noop state DOWN qlen 1000 link/ether 02:ba:6f:0c:21:c4 brd ff:ff:ff:ff:ff:ff 3 lan1@eth0: &lt;BROADCAST,MULTICAST,M-DOWN&gt; mtu 1500 qdisc noop state DOWN qlen 1000 link/ether 02:ba:6f:0c:21:c4 brd ff:ff:ff:ff:ff:ff 4 lan2@eth0: &lt;BROADCAST,MULTICAST,M-DOWN&gt; mtu 1500 qdisc noop state DOWN qlen 1000 link/ether 02:ba:6f:0c:21:c4 brd ff:ff:ff:ff:ff:ff 5 lan3@eth0: &lt;BROADCAST,MULTICAST,M-DOWN&gt; mtu 1500 qdisc noop state DOWN qlen 1000 link/ether 02:ba:6f:0c:21:c4 brd ff:ff:ff:ff:ff:ff 6 lan4@eth0: &lt;BROADCAST,MULTICAST,M-DOWN&gt; mtu 1500 qdisc noop state DOWN qlen 1000 link/ether 02:ba:6f:0c:21:c4 brd ff:ff:ff:ff:ff:ff</code></pre> <p>I have the <code>eth0</code> device here like normal and then I have the 4 interfaces for the ports on the switch I defined in the device tree. To make it actually do something the interfaces actually need to be brought online first:</p> <pre><code>$ ip link set eth0 up $ ip link set lan1 up $ ip link set lan2 up $ ip link set lan3 up $ ip link set lan4 up $ ip link 1: lo: &lt;LOOPBACK&gt; mtu 65536 qdisc noop state DOWN qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: eth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1508 qdisc mq state UP qlen 1000 link/ether 02:ba:6f:0c:21:c4 brd ff:ff:ff:ff:ff:ff 3: lan1@eth0: &lt;NO-CARRIER,BROADCAST,MULTICAST,UP&gt; mtu 1500 qdisc noqueue state LOWERLAYERDOWN qlen 1000 link/ether 02:ba:6f:0c:21:c4 brd ff:ff:ff:ff:ff:ff 4: lan2@eth0: &lt;NO-CARRIER,BROADCAST,MULTICAST,UP&gt; mtu 1500 qdisc noqueue state LOWERLAYERDOWN qlen 1000 link/ether 02:ba:6f:0c:21:c4 brd ff:ff:ff:ff:ff:ff 5: lan3@eth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue state UP qlen 1000 link/ether 02:ba:6f:0c:21:c4 brd ff:ff:ff:ff:ff:ff 6: lan4@eth0: &lt;NO-CARRIER,BROADCAST,MULTICAST,UP&gt; mtu 1500 qdisc noqueue state LOWERLAYERDOWN qlen 1000 link/ether 02:ba:6f:0c:21:c4 brd ff:ff:ff:ff:ff:ff</code></pre> <p>Now the switch is up you can see I have a cable plugged into the third port. This system hooks into a lot of the Linux networking so it Just Works(tm) with a lot of tooling. Some examples:</p> <ul><li>Add a few of the lan ports into a standard Linux bridge and the switchdev system will bridge those ports together in the switch chip so Linux doesn&#x27;t have to forward that traffic.</li> <li>Thinks like <code>ethtool lan3</code> just work to get information about the link. and with <code>ethtool -S lan3</code> all the standard status return info which includes packets that have been fully handled by the switch.</li> </ul> <h2>Limitations</h2> <p>There's a few things that makes this not very nice to work with. First of all the requirement of either building a custom network switch or tearing open an existing one and finding the right connections. </p> <p>It's not really possible to use this system on regular computers/servers since you need device trees to configure the kernel for this and most computers don't have kernel-controlled GPIO pins available to hook up a switch.</p> <p>As far as I can find there's also no way to use this with a network port on the computer side that's not fixed, USB network interfaces don't have a device tree node handle to refer to to set the conduit port.</p> <p>There is a chance some of these limitations are possible to work around, maybe there's some weird USB device that exposes pins on the GPIO subsystem, maybe there's a way to load switchdev without being on an ARM device but that would certainly take a bit more documentation...</p> Building a DSMR reading boardhttps://blog.brixit.nl/building-a-dsmr-reading-board/101ElectronicsMartijn BraamMon, 27 May 2024 10:08:00 -0000<p>Quite a while back I designed a small PCB for hooking up sensors to an ESP8266 module to gather data and have a nice Grafana dashboard with temperature readings. While building this setup I grabbed one of my spare ESP8266 dev boards and soldered that to the P1 port on my smart energy meter to log the power usage of the whole house.</p> <p>For those unfamiliar with P1 and DSMR since it's quite regional: DSMR is the Dutch Smart Meter Requirements specification. It's a document that describes the connectivity of various ports on energy meters and it's used in the Netherlands and a few countries around it. One of the specifications within DSMR is P1 which is the document for the RJ12 connector for plugging third party monitoring tools.</p> <h2>Electrical design</h2> <p>So the first part in the project is figuring out how to make the hardware itself work. I've decided to slightly modernize my design so this is the first module I build that uses an ESP32 chip instead of the older ESP8266 I have in use everywhere.</p> <p>I specifically designed the board around the ESP32-S3-WROOM-1 module. This makes things significantly simpler than my older designs since there's no programming circuitry required.</p> <p>The design contraints I've used for this board are:</p> <ul><li>Make the design handsolderable instead of using JLCPCB assembly service for the boards. I enjoy designing boards and I probably should get some more actual soldering experience with SMD parts.</li> <li>Connect to the smart energy meter using off the shelf RJ12 cables instead of soldering wires on the board. Also include a passive P1 splitter on the board since it&#x27;s becoming more and more popular to have charging points for electric cars which can also be hooked up to the P1 port.</li> <li>USB-C for programming and power. I have a back-up programming header footprint on the board but it shouldn&#x27;t be necessary . Just like putting USB-B Micro ports on devices shouldn&#x27;t be allowed anymore in 2024.</li> </ul> <p>The schematic is basically the absolute minimum required to get the ESP32 module up and running. Since the ESP32-S3 has native USB support it means that I can drop a whole bunch of parts from the schematic that is normally required for programming.</p> <p>Another neat feature is that this chip has a fully connected I/O mux inside which means I can basically pick any random GPIO pin and later configure in software which hardware block in the chip it hooks up to. This feature is also available in other chips like the RP2040 but there it's way more limited. There is only a few valid choices for pins for UART1 for example and there's no way to swap RX and TX on a board without using a soldering iron.</p> <p>In the DSMR design I'll only be using a single GPIO pin configured to be the RX of one of the hardware UARTS so it can receive the data.</p> <figure class="kg-card kg-image-card kg-width-wide"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1716744001/image.png" class="kg-image"><figcaption>The full schematic for the DSMR module</figcaption></figure> <p>The board has a jumper on it to pick how the data requests are handled. In one position the line is connected straight to 5V so the energy meter will just continuously send the data over the P1 port, which should be correct in most situations. The other mode connects the data request line to the pass-through port so the device connected after the DSMR module can select when the data should be sent, in this case the module will just be passively sniffing the traffic whenever it's sent.</p> <p>There is also a solder jumper on the board to select whether it's powered from the USB connection or from the P1 port itself. According to the P1 specifications the power supplied by most energy meters won't be enough to reliably run the ESP32 module so there is always to option to power it from the USB-C port. In most cases there will be random device like a router nearby that has a powered USB connection to run the module.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1716744605/image.png" class="kg-image"><figcaption>The PCB for the DSMR module</figcaption></figure> <p>I managed to fit everything on a small 40x40mm PCB by having the the two RJ12 connectors hang off the board. This also makes those neatly line up with the edges of the case for this board. The ESP32 module also hangs off the edge of the board, pictured at the top here. This is because the antenna part of the ESP module has a keepout area where there shouldn't be any copper on the board anyway to not disturb the WiFi signal.</p> <p>To make it fit I've also moved the power regulator and a few passives to the bottom side of the board. Normally it would be pretty expensive to do this but since I'm hand soldering these boards using both sides of the board is free.</p> <p>Another upside with handsoldering the boards is that I'm not limited to the JLCPCB parts for once and I can just whatever random parts I can source. I've decided to get these boards made by Aisler this time so the boards are made in Germany instead of China and the boards just look great. I normally use the KiCad Aisler Push plugin in my workflow anyway just to run some sanity checks on the board design in addition to the checks ran in KiCad.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1716745405/image.png" class="kg-image"><figcaption>Some PCB design notes generated by Aisler</figcaption></figure> <p>In this screenshot it's just warning me about the holes for the plastic pins in the footprint of the USB-C connector. It doesn't matter whether those are copper plated or not so I just ignore this warning. After receiving the boards I've also checked this and the holes did not get copper plated at all.</p> <p>I also noticed there was a <a href="https://community.aisler.net/t/introducing-design-in-europe-by-aisler-and-wurth-elektronik/3886">discount for using components from Würth Elektronik</a> on the board so naturally I took that as a challenge to see how for I can push that. It turns out that in this design the answer is pretty far: The easy thing is to use WE part numbers for the passives on the board.</p> <p>The second thing I replaced is the connectors on the board. I was already pretty happy I did not have to make custom footprints for once because that's just a time consuming job. After comparing a lot of footprints it turns out that WE makes an RJ12 connectors with the exact footprint I had already made. The USB-C connector is a bit more difficult since I'm used to picking the one available at JLCPCB that also happens to have a footprint already in KiCad. It turns out that the Würth Elektronik USB-C connector fits on the <code>USB_C_Receptacle_GCT_USB4105-xx-A_16P_TopMnt_Horizontal</code> footprint in KiCad. I guess there's simply not too many different ways to make spec compliant USB-C connectors.</p> <h2>The soldering</h2> <p>So the board is relatively straightforward to solder, it doesn't have that many parts and I decided to not pick the tiniest SMT parts I could find. If you're not familiar with SMT parts, they get delivered on reels and if you order small quantities you get a smaller cut-off portion of a reel. Here's some capacitors for example:</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1716749877/20240526_0008.jpg" class="kg-image"></figure> <p>These are the 22uF 0805 capacitors I've ordered for the ESP32 power rail. The rest of the passives are the slightly smaller 0603 packages. On the right of the board you can see the pads for the 0805 capacitor and the 0603 capacitor side by side. In this case the Würth capacitors came in a neat transparent piece of tube. If you order a full reel of cheap resistors they usually come on paper tape which looks like this:</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1716750066/IMG_20240526_205530.jpg" class="kg-image"><figcaption>This is 5000 resistors in 0603 size</figcaption></figure> <p>This is probably one of few times I've ever ordered 5000 of something. Even the bigger parts like the ESP32 modules come on a reel:</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1716750232/IMG_20240526_210232.jpg" class="kg-image"><figcaption>This strip holds 20 cores of pure computing power :D</figcaption></figure> <p>This soldering itself was quite time consuming but not that hard. The secret is to just use a lot of flux and not use plastic tweezers that melt while soldering. </p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1716750417/20240526_0020.jpg" class="kg-image"></figure> <p>Maybe after some experience I can even get them on straight. Supposedly it's a lot easier and neater to use solder paste and stencils to do this so I also ordered the stencils for this PCB. I don't have the soldering paste and extra equipment for it though so that experiment will have to wait a bit longer.</p> <h2>The case</h2> <p>To have this look somewhat neat when it's finished I also need a case for the board. The need for cases has annoyed me several times already when designing PCBs. The options are either grabbing an off the shelf project box which is usually expensive and never fits exactly with the design or just 3D printing a case which involves me angrily staring at CAD software to figure out how to make things fit and match up to my PCB.</p> <p>I've already written a blog post about my solution here which is <a href="https://blog.brixit.nl/automatic-case-design-for-kicad/">TurboCase</a>. It's a tool that automatically generates a case based on the PCB file from KiCad. I wrote the software specifically because I needed it for this PCB so naturally the case generated by it works perfectly for this project :)</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1716746584/image.png" class="kg-image"><figcaption>TurboCase generated case for this board</figcaption></figure> <p>Since writing the blog post about the details on how TurboCase works I've added support for TurboCase-specific footprints. I now have a global KiCad library that holds footprints that are designed to go on the <code>User.6</code> layer to add prefab features to the final OpenSCAD file like screw holes in the case and the hole for by USB-C connectors.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1716746734/image.png" class="kg-image"><figcaption>The contents of the User.6 layer of my board design</figcaption></figure> <p>Here you can see the drawing on the <code>User.6</code> layer of the DSMR module. It contains several features:</p> <ul><li>The outline of the case drawn using the graphic elements in KiCad. This functions the same way as the board outline normally works on the <code>Edge.Cuts</code> layer except it describes the outline of the inside of the case.</li> <li>Three mounting holes that fit countersunk M3 sized screws to mount the case on the wall.</li> <li>A M3-sized keyhole screw-mounting thingy so the board can be mounted in a less permanent way.</li> <li>The cube with the line in it marks a spot where turbocase makes an USB-C shaped hole matched up perfectly to the USB-C connector on the board.</li> <li>The RJ12 connectors already cross the border of the case in KiCad so there will be holes generated for them automatically in the case based on the connector outline on the fabrication layer.</li> <li>The 3 MountingHole footprints on the PCB will be used to generate mounting posts for the PCB inside the case, the main reason why I started making this tool to make sure those things align perfectly every time.</li> </ul> <p>I did a few iterations on the footprint library and did a series of test prints of the case to make sure everything is correct and I can happily report that everything just matches up perfectly.</p> <p>The few days I spend building and testing this tool now narrows down the whole process from kicad to physical product down to:</p> <ul><li>Draw outline in KiCad</li> <li>Run TurboCase on the <code>.kicad_pcb</code> file to get my OpenSCAD file.</li> <li>Export the OpenSCAD file to .STL and 3D print it.</li> </ul> <p>I hope this utility is useful for more people and I would love to see how it performs with other board designs!</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1716747340/darktable.ZQFGO2.jpg" class="kg-image"></figure> <h2>The software side</h2> <p>The more annoying part of the whole process was dealing with the software. For the ESP8266 I've always used the Arduino IDE. This is mainly because I did not want to deal with the xtensa toolchain required to build software for these chips. I don't do complicated things with these modules and with the Arduino IDE they simply always just worked.</p> <p>Switching over to the ESP32 did mean that stuff stopped working automatically for me sadly, the base software worked great but I had some troubly getting the MQTT library to work. The dependency resolution in the Arduino IDE is very simple and it does not account for having different dependencies with the same include name so I tried PlatformIO for this project.</p> <p>The whole setup process for PlatformIO was pretty smooth until I got to platform selection. There's a few different variations of the ESP32-S3 module available and even a few more specific variations of the WROOM-1 module I put on my board. I dit however commit the sin of picking the 4MB flash version of the board. This is the only one that doesn't have a specific config in PlatformIO and the merge request for it was denied based on that it would be easy to override the platform config with a json file. I did not find how to do that easily so I picked one of the random dev boards that used the wroom-1 module with 4MB flash instead to use whatever changes they have made to the JSON file for the board to make this work.</p> <p>Now for actually implementing the P1 protocol: This is deceptively hard. The way the protocol works is that you pull the data request line high on the connector and then the energy meter will start spewing out datagrams over the serial port on either a 1 second or 10 second interval depending on the protocol version. These datagrams look something like this:</p> <pre><code>/XMX5LGBBFG10 1-3:0.2.8(42) 0-0:1.0.0(170108161107W) 0-0:96.1.1(4530303331303033303031363939353135) 1-0:1.8.1(002074.842*kWh) 1-0:1.8.2(000881.383*kWh) 1-0:2.8.1(000010.981*kWh) 1-0:2.8.2(000028.031*kWh) 0-0:96.14.0(0001) 1-0:1.7.0(00.494*kW) 1-0:2.7.0(00.000*kW) 0-0:96.7.21(00004) 0-0:96.7.9(00003) 1-0:32.32.0(00000) 1-0:32.36.0(00000) 0-0:96.13.1() 0-0:96.13.0() 1-0:31.7.0(003*A) 1-0:21.7.0(00.494*kW) 1-0:22.7.0(00.000*kW) 0-1:24.1.0(003) 0-1:96.1.0(4730303139333430323231313938343135) 0-1:24.2.1(170108160000W)(01234.000*m3) !D3B0</code></pre> <p>This looks to be a relatively simple line based protocol. The fields are identified by a numeric code called the "OBIS" code. This stands for OBject Identification System. Then there's values added in parenthesis after it. The difficulty in parsing this is that there can be multiple values in parentheses added after each OBIS field. This would not be terrifically hard to parse if the DSMR spec had nailed down the encoding a bit more but it merly specifies that the field ends in a newline.</p> <p>In my case some of the values themselves also contain newlines so this breaks the assumption you can parse this based simply based on lines and that combined by the variable (but unspecified) amount of values means that the parser code for this becomes quite nasty.</p> <p>Luckily I got to use one of the features I gained by switching to PlatformIO: Unit testing. The whole DSMR parser is moved to it's own class in the codebase and only gets a <code>Stream</code> reference to parse the DSMR datagrams from. This means I don't have to sit with my laptop in the hallway debugging the parser and I can check if the parser works with dumps from various smart energy meters.</p> <p>This all glued together means that the DSMR module will get the data from the P1 port and then sent it out over WiFi to my MQTT server with a separate topic for every field. And the result:</p> <figure class="kg-card kg-image-card kg-width-wide"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1716749436/image.png" class="kg-image"><figcaption>Grafana showing the power consumption and production as measured by the smart energy meter</figcaption></figure> <p>The source for the hardware and firmware is available at <a href="https://git.sr.ht/~martijnbraam/dsmr-module">https://git.sr.ht/~martijnbraam/dsmr-module</a> and the board itself is also available on <a href="https://aisler.net/p/IWCEGPWL">https://aisler.net/p/IWCEGPWL</a></p> Automatic case design for KiCadhttps://blog.brixit.nl/automatic-case-design-for-kicad/100ElectronicsMartijn BraamWed, 15 May 2024 19:48:27 -0000<p>I don't generally get along great with CAD software with the exception of KiCad. I guess the UX for designing things is just a lot simpler when you only have 2 dimensions to worry about. After enjoying making a PCB in KiCad the annoying for me is always getting a case designed to fit the board.</p> <p>If I'm lucky I don't need many external holes to fit buttons or connectors and if I'm really lucky the mounting holes for the board are even in sensible locations. I wondered if there was a quick way to get the positions of the mounting holes into some 3D CAD software to make the mounting posts in the right position without doing math or measuring.</p> <p>But what's even better than importing mounting hole locations? Not having to build the case at all!</p> <h2>Turbocase</h2> <p>So the solution is just several hundred lines of Python code. I've evaluated a few ways of getting data out of KiCad to mess with it and initially the <code>kicad-cli</code> tool looked really promising since it allows exporting the PCB design to several vector formats without launching KiCad. After exporting a few formats and seeing how easy it would be to get the data into Python I remembered that the PCB design files are just s-expressions, so the easiest way is just reading the .kicad_pcb file directly.</p> <figure class="kg-card kg-image-card kg-width-wide"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1715799912/image.png" class="kg-image"><figcaption>A snippet of the .kicad_pcb source file</figcaption></figure> <p>So with this there's several pieces of data the tool extracts for the case design:</p> <ul><li>The footprints with <code>MountingHole</code> in the name to place posts for threaded metal inserts in my 3d prints</li> <li>A case outline from a user layer. This has the same semantics as the Edge.Cuts layer except that it defines the shape of the inner edge of the case.</li> <li>Information about connectors to make holes in the case and to have placeholders in the final OpenSCAD file for easier modifications.</li> </ul> <p>The locations of the mounting holes is pretty easy to get up and running. By iterating over the PCB file the tool saves all the footprints that are mounting holes and for each of those footprints it locates the pad with the largest hole in it and saves that to make a mounting post in the case.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1715800268/image.png" class="kg-image"></figure> <p>In my test PCB I already had some nice edge-cases to deal with since the specific footprint I used has vias in it which also counts as pads. The outer side of the pad is used as the diameter of the mounting post that will be generated and inside that a hole will be created that fits the bag of threaded metal inserts I happen to have here.</p> <h2>Case outlines</h2> <p>To make the actual case I initially planned to just grab the PCB outline and slightly enlarge it as a template. This turns out to have a few obvious flaws, for example the ESP32 module that I have hanging over the edge of the PCB. Grabbing the bounding box of the entire design would also be a quick fix but that would mean the case would be way too large again due to the keepout area of the ESP32 footprint. The solution is manually defining the shape of the case since this gives the most flexibility.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1715800592/image.png" class="kg-image"></figure> <p>I picked the <code>User.6</code> layer as the case outline layer since it has a neat blue color in this KiCad theme. The semantics for the case outline is the same as what you'd normally do on the <code>Edge.Cuts</code> layer except that this defines the inner wall of the case. The turbocase utility will then add a wall thickness around it to make the 3D model of the case.</p> <h2>Connector holes</h2> <p>What use is a case without any holes for connections? This turned out to be a more difficult issue. For the connectors I would actually need some height information and KiCAD is still very much 2D. It is possible to link a 3D model to the footprint which is great to see how the final board will look like:</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1715801016/image.png" class="kg-image"></figure> <p>Sadly using this 3D model data would be quite difficult for turbocase since it would require an importer for all the various supported 3D model formats and then a way to grab the outline of a slice of the model.</p> <p>So I picked the uglier but simpler solution. Just give the connector a height in some metadata and treat them as cubes. For the footprint I use the bounding box of the <code>F.Fab</code> layer which should correspond relatively closely to the size of the connector. To store the 3rd dimension I simply added a property to the connectors I wanted to be relevant to the case design:</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1715801208/image.png" class="kg-image"></figure> <h2>Exporting the case</h2> <p>I decided to export the cases as OpenSCAD files. This is mostly because these are simple text files I can generate and I already have some experience with OpenSCAD design.</p> <p>A large part of the generated file is boilerplate code for doing the basic case components. After that it will export the case outline as a polygon and do the regular OpenSCAD things to it to make a 3D object.</p> <div class="highlight"><pre><span></span><span class="n">standoff_height</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">5</span><span class="p">;</span> <span class="n">floor_height</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">1.2</span><span class="p">;</span> <span class="n">pcb_thickness</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">1.6</span><span class="p">;</span> <span class="n">inner_height</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">standoff_height</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">pcb_thickness</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mf">11.5</span><span class="p">;</span> <span class="n">pcb_top</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">floor_height</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">standoff_height</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">pcb_thickness</span><span class="p">;</span> <span class="n">box</span><span class="p">(</span><span class="mf">1.2</span><span class="p">,</span><span class="w"> </span><span class="mf">1.2</span><span class="p">,</span><span class="w"> </span><span class="n">inner_height</span><span class="p">)</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="n">polygon</span><span class="p">(</span><span class="n">points</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[[</span><span class="mi">102</span><span class="p">,</span><span class="mi">145</span><span class="p">],</span><span class="w"> </span><span class="p">[</span><span class="mi">140</span><span class="p">,</span><span class="mi">145</span><span class="p">],</span><span class="w"> </span><span class="p">....</span><span class="w"> </span><span class="p">]);</span> <span class="p">}</span> </pre></div> <p>The pcb_thickness is also one of the variables exported from the KiCad PCB file and the <code>box(wall, bottom, height)</code> module creates the actual basic case.</p> <p>For the connector a series of cubes is generated and those are substracted from the generated box:</p> <div class="highlight"><pre><span></span><span class="c1">// J1 Connector_USB:USB_C_Receptacle_GCT_USB4105-xx-A_16P_TopMnt_Horizontal USB 2.0-only 16P Type-C Receptacle connector</span> <span class="n">translate</span><span class="p">([</span><span class="mf">104.15</span><span class="p">,</span><span class="w"> </span><span class="mi">119</span><span class="p">,</span><span class="w"> </span><span class="n">pcb_top</span><span class="p">])</span> <span class="w"> </span><span class="n">rotate</span><span class="p">([</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">90</span><span class="p">])</span> <span class="w"> </span><span class="cp">#connector(-4.47,-3.675,4.47,3.675,3.5100000000000002);</span> <span class="c1">// J2 Connector_RJ:RJ12_Amphenol_54601-x06_Horizontal </span> <span class="n">translate</span><span class="p">([</span><span class="mi">115</span><span class="p">,</span><span class="w"> </span><span class="mf">131.9</span><span class="p">,</span><span class="w"> </span><span class="n">pcb_top</span><span class="p">])</span> <span class="w"> </span><span class="n">rotate</span><span class="p">([</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">90</span><span class="p">])</span> <span class="w"> </span><span class="cp">#connector(-3.42,-1.23,9.78,16.77,11.7);</span> <span class="c1">// J3 Connector_RJ:RJ12_Amphenol_54601-x06_Horizontal </span> <span class="n">translate</span><span class="p">([</span><span class="mf">127.11</span><span class="p">,</span><span class="w"> </span><span class="mf">138.26</span><span class="p">,</span><span class="w"> </span><span class="n">pcb_top</span><span class="p">])</span> <span class="w"> </span><span class="n">rotate</span><span class="p">([</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">270</span><span class="p">])</span> <span class="w"> </span><span class="cp">#connector(-3.42,-1.23,9.78,16.77,11.7);</span> </pre></div> <p>This also has some extraced metadata from the PCB to figure out what's what when editing the .scad file. The <code>connector</code> module is a simple helper for generating a cube from a bounding box that accounts for the origin of the connector not being in the center.</p> <p>Finally the mounting posts are added to the case as simple cylinders:</p> <div class="highlight"><pre><span></span><span class="c1">// H1</span> <span class="n">translate</span><span class="p">([</span><span class="mi">105</span><span class="p">,</span><span class="w"> </span><span class="mi">108</span><span class="p">,</span><span class="w"> </span><span class="mf">1.2</span><span class="p">])</span> <span class="w"> </span><span class="n">mount</span><span class="p">(</span><span class="mf">3.4000000000000004</span><span class="p">,</span><span class="w"> </span><span class="mf">6.4</span><span class="p">,</span><span class="w"> </span><span class="mi">5</span><span class="p">);</span> <span class="c1">// H3</span> <span class="n">translate</span><span class="p">([</span><span class="mi">121</span><span class="p">,</span><span class="w"> </span><span class="mi">140</span><span class="p">,</span><span class="w"> </span><span class="mf">1.2</span><span class="p">])</span> <span class="w"> </span><span class="n">mount</span><span class="p">(</span><span class="mf">3.4000000000000004</span><span class="p">,</span><span class="w"> </span><span class="mf">6.4</span><span class="p">,</span><span class="w"> </span><span class="mi">5</span><span class="p">);</span> <span class="c1">// H2</span> <span class="n">translate</span><span class="p">([</span><span class="mi">137</span><span class="p">,</span><span class="w"> </span><span class="mi">108</span><span class="p">,</span><span class="w"> </span><span class="mf">1.2</span><span class="p">])</span> <span class="w"> </span><span class="n">mount</span><span class="p">(</span><span class="mf">3.4000000000000004</span><span class="p">,</span><span class="w"> </span><span class="mf">6.4</span><span class="p">,</span><span class="w"> </span><span class="mi">5</span><span class="p">);</span> </pre></div> <p>The extracted information from the mounting hole is the 3.2mm drill diameter and the 6.2mm pad diameter. The inner hole is expanded by 0.2mm in this case to make the holes work with the metal inserts.</p> <h2>Further improvements</h2> <p>There's a lot of neat things that could be added to this. The major one being a lid for the case. This also would need a bunch more configurability to deal with mounting mechanisms for the lid like screw holes or some clips.</p> <p>The system could also be extended by producing a footprint library specifically for turbocase to signify where to add specific features to the case. This could be things like cooling holes, led holes. Maybe some fancier connector integration.</p> <p>The output from turbocase also suffers from the same issue a lot of OpenSCAD designs suffer from: it's very hard to add chamfers and fillets to make a less rectangular case. That would require someone with more OpenSCAD knowledge to improve the generated output.</p> <p>The source code for the turbocase tool is available at <a href="https://sr.ht/~martijnbraam/turbocase/">https://sr.ht/~martijnbraam/turbocase/</a> and the utility is available on pypi under the <code>turbocase</code> name.</p> Megapixels contributionshttps://blog.brixit.nl/megapixels-contributions/99MegapixelsMartijn BraamSat, 11 May 2024 14:45:17 -0000<p>I've been working on the code that has become libmegapixels for a bit more as a year now. It has taken several thrown-away codebases to come to a general architecture I was happy with and it it has been quite a task to split off media pipeline tasks from the original Megapixels codebase.</p> <p>After staring at this code for many months I thought I've made libmegapixels a nearly perfect little library. That's the problem with working on a codebase without anyone else looking at it.</p> <p>About two weeks ago libmegapixels and the general Megapixels 2.x codebase had it's first contact with external contributors and that has put a spotlight on all the low hanging fruit in documentation and codebase issues. A great example of that is this commit:</p> <div class="highlight"><pre><span></span><span class="gh">diff --git a/src/parse.c b/src/parse.c</span> <span class="gh">index bfea3ec..93072d0 100644</span> <span class="gd">--- a/src/parse.c</span> <span class="gi">+++ b/src/parse.c</span> <span class="gu">@@ -403,6 +403,8 @@ libmegapixels_load_file(libmegapixels_devconfig *config, const char *file)</span> <span class="w"> </span> config_init(&amp;cfg); <span class="w"> </span> if (!config_read_file(&amp;cfg, file)) { <span class="w"> </span> fprintf(stderr, &quot;Could not read %s\n&quot;, file); <span class="gi">+ fprintf(stderr, &quot;%s:%d - %s\n&quot;,</span> <span class="gi">+ config_error_file(&amp;cfg), config_error_line(&amp;cfg), config_error_text(&amp;cfg));</span> <span class="w"> </span> config_destroy(&amp;cfg); <span class="w"> </span> return 0; <span class="w"> </span> } </pre></div> <p>A simple patch that massively improves the usablility for people writing libmegapixels config files: Actually printing the parsing errors from libconfig when a file could not be read. Because I generally run libmegapixels through the IDE and have all the syntax highlighting etc set up for the files I simply haven't triggered this codepath enough to actually implement this part.</p> <p>These last two weeks there have also been some significantly more complicated fixes like tracing segfault issues in Megapixels 2.x which helps a lot with getting the new codebase ready for daily use. Figuring out some API issues in libmegapixels like not correctly setting camera indexes in the returned data. Also the config files have now been updated to work with the latest versions of the PinePhone Pro kernel instead of the year old build I've been developing against.</p> <h2>Video recording</h2> <p>I've been saying for a long time that video recording on the PinePhone won't be possible, especially not to the level of support on Android and iOS due to hardware limitations. The only real hope for proper video recording would be that someone gets H.264 hardware encoding to work on the A64 processor.</p> <p>I can happily report that I was wrong. Pavel Machek has made significant progress in PinePhone video recording with a few large contributions that implement the UI bits to add video recording. A new second postprocessing pipeline for running external video encoding scripts just like Megapixels already lets you write your own custom scripts for processing the raw pictures into JPEGs.</p> <p>Video recording is a complicated issue though, mainly due to the sheer amount of data that needs to be processed to make it work smoothly. On the maximum resultion of the sensor in the PinePhone the framerate isn't high enough for recording normal videos (unless you enjoy 15fps video files) but on lower resolutions the pipeline can run at normal video framerates. The maximum framerates from the sensor for this are 1080p at 30fps and 720p at 60fps.</p> <p>For 720p60 the bandwidth of the raw sensor data is 442 Mbps and for 1080P30 this is 497 Mbps. This is a third of the expected bandwidth because the raw sensor data is essentially a greyscale image where every pixels has a different color filter in front. This is too much data to write out to the eMMC or SD card to process later and the PinePhone also struggles already to encode 720p30 video live without even running a desktop environment.</p> <p>There are two implementations of video recording right now. One that saves the raw DNG frames to a tmpfs since RAM is the only thing that can keep up with the data rate. This should give you roughly 30 seconds of video recording capabilities and after that recording time it will take a while to actually encode the video.</p> <p>Pavel has posted an <a href="https://social.kernel.org/notice/AhFxeCMdslrRIhQjE8">example of this video recording</a> on his mastodon.</p> <p>The second way is putting the sensor in a YUV mode instead of raw data. This gives worse picture quality on the sensor in the PinePhone but the data format matches more closely to the way frames are stored in video files so the expensive debayer step can be skipped while video recording. This together with encoding H.264 video with the ultrafast preset should make it just about possible to record real-time encoded video on the PinePhone.</p> <h2>Many thanks</h2> <p>It's great to see contributions to Megapixels 2 and libmegapixels. It's a big step towards getting the Megapixels 2.x codebase production ready and it's simply a lot more fun to work on a project together with other people.</p> <p>It's great to have contributors working on the UI code, the camera support fixes for devices and the many bugfixes to the internals. It's also very helpful to actually have issues created by people building and testing the code on other distributions. This already ironed out a few issues in the build system.</p> <p>There also has been some nice contributions to the Megapixels 1.x codebase, all of those should by now already have been merged into your favorite PinePhone distribution :)</p> <p>The last few Megapixels update blogposts have all been around Megapixels 2.x and the supporting libraries so none of the improvements are immediately usable by actual PinePhone{,Pro} and Librem 5 users until there is an actual release. It will take a bunch more polish until feature parity with Megapixels 1.x is reached.</p> Moving to a RTOS on the RP2040https://blog.brixit.nl/moving-to-a-rtos-on-the-rp2040/96ElectronicsMartijn BraamMon, 06 May 2024 15:58:55 -0000<p>I've been working on a bunch of small projects involving microcontrollers. Currently a lot of them are based around the Raspberry Pi Pico boards because I like the development experience of those a lot. They have a decent SDK and cheap hardware to get started and the debugger works with gdb/openocd so it just integrates in all IDEs that support that.</p> <p>One of my current projects is making a fancy hardware controller for a bunch of video equipment I use. The main things that will be controlled are two PTZ cameras (those are cameras that have motors to move them). One stationary camera and the video switching equipment that that's hooked up to.</p> <p>Currently the control of the control of the PTZ cameras is done with an unbranded panel that looks suspiciously like the Marshall VS-PTC-200:</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1715003106/VS-PTC-200-Keyboard-PTZ-Compact-Controller.jpg" class="kg-image"><figcaption>(Image from marshall-usa.com)</figcaption></figure> <p>The performance of this controller is simply not very great, especially for the price. It was a €650 device several years ago and for that money it has very annoying squishy buttons and the cheapest analog joystick you could find. Most of the buttons are also not functional with the cameras in use since this seems to be optimized for security cameras. This connects to the cameras over an RS-485 bus.</p> <p>The second thing I want my panel to do is very basic ATEM video switcher control. Currently that's fully done using the software panel on the computer because the panels from Blackmagic Design are very expensive.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1715003565/image.png" class="kg-image"><figcaption>There&#x27;s a tiny cheaper one now though. (from blackmagicdesign.com)</figcaption></figure> <p>After a bit of designing I figured the most minimal design I can get away with is 9 buttons, the joystick and a display for the user interface. The hardware design has gone through several iterations over the last year but I now have some PCBs with the 9 RGB buttons on it, the $10 joystick that was also in the Marshall-clone panel and to interface with the outside world it has the TP8485E to communicate with the cameras over RS-485 and a Wiznet W5500 module to communicate with the video switcher over ethernet.</p> <figure class="kg-card kg-image-card"><img src="https://blog.brixit.nl/image/w1000/static//static/files/blog.brixit.nl/1715003933/IMG_20240429_184436.jpg" class="kg-image"><figcaption>This includes a bunch of &quot;oops-the-wrong-pinout&quot; fixes...</figcaption></figure> <p>After a lot of fixing of the board I had made I now have all the hardware parts functional, but the difficult part of this project is the software.</p> <h2>Initial software</h2> <p>I first started creating the software like I do all the RP2040 based projects. A cmake project that pulls in the pico-sdk. To make anything work at all I dedicated the second core of the pico to dealing with the Wiznet module and the first core then handles all the user interface I/O. This worked fine to blink some leds and I did implement a DHCP client that ran on the second core. It did make implementing the rest of the system a lot more complicated. There's simply a lot of things that need to happen at once:</p> <ul><li>Draw an user interface on the display that&#x27;s somewhat smooth</li> <li>Send out VISCA commands over the RS-485 interface</li> <li>Respond to button presses</li> <li>Keep the entire network stack alive with multiple connections</li> </ul> <p>There's a bunch of things that need to happen on the network, the first of which is some actually standards complicant DHCP support. This would require keeping track of the expire times and occasionally talk to the DHCP server to keep the lease active. The second background task is making mDNS work. The ATEM video switcher IP can be autodiscovered using DNS-SD and it would be great to also announce the existence of the control panel.</p> <p>The ATEM protocol itself is also one of the harder parts to get right, the protocol itself is pretty simple but it does involve sometimes receiving a lot of data that exceeds the buffer size of the Wiznet module and the protocol has a very low timeout for disconnection for when you stop sending UDP datagrams to the ATEM.</p> <p>This all made me decide that it's probably better to switch to an RTOS for this project.</p> <h2>FreeRTOS</h2> <p>The first project I've looked into is FreeRTOS. This is technically already bundled inside the pico-sdk but all tutorials I've found for this download a fresh copy anyway so that's what I did. FreeRTOS seems to be the simplest RTOS I've looked at from this list, the main thing it provides is the RTOS scheduler and some communication between tasks. The simplest way I can show it is with some code:</p> <div class="highlight"><pre><span></span><span class="cp">#include</span><span class="w"> </span><span class="cpf">&quot;FreeRTOS.h&quot;</span> <span class="n">TaskHandle_t</span><span class="w"> </span><span class="n">button_task</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">NULL</span><span class="p">;</span> <span class="n">TaskHandle_t</span><span class="w"> </span><span class="n">led_task</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">NULL</span><span class="p">;</span> <span class="n">QueueHandle_t</span><span class="w"> </span><span class="n">led_queue</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">NULL</span><span class="p">;</span> <span class="kt">void</span><span class="w"> </span><span class="nf">buttonTask</span><span class="p">(</span><span class="kt">void</span><span class="w"> </span><span class="o">*</span><span class="n">param</span><span class="p">)</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">while</span><span class="w"> </span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="kt">bool</span><span class="w"> </span><span class="n">state</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">get_button_pressed</span><span class="p">();</span> <span class="w"> </span><span class="n">xQueueSend</span><span class="p">(</span><span class="n">led_queue</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="n">state</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">);</span> <span class="w"> </span><span class="p">}</span> <span class="p">}</span> <span class="kt">void</span><span class="w"> </span><span class="nf">ledTask</span><span class="p">(</span><span class="kt">void</span><span class="w"> </span><span class="o">*</span><span class="n">param</span><span class="p">)</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">while</span><span class="w"> </span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="kt">bool</span><span class="w"> </span><span class="n">state</span><span class="p">;</span> <span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="n">xQueueReceive</span><span class="p">(</span><span class="n">led_queue</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="n">state</span><span class="p">,</span><span class="w"> </span><span class="n">portMAX_DELAY</span><span class="p">))</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="n">gpio_put</span><span class="p">(</span><span class="n">LED_PIN</span><span class="p">,</span><span class="w"> </span><span class="n">state</span><span class="p">);</span> <span class="w"> </span><span class="p">}</span> <span class="w"> </span><span class="p">}</span> <span class="p">}</span> <span class="kt">int</span><span class="w"> </span><span class="nf">main</span><span class="p">()</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="n">xTaskCreate</span><span class="p">(</span><span class="n">buttonTask</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;Button&quot;</span><span class="p">,</span><span class="w"> </span><span class="mi">128</span><span class="p">,</span><span class="w"> </span><span class="nb">NULL</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="n">button_task</span><span class="p">);</span> <span class="w"> </span><span class="n">xTaskCreate</span><span class="p">(</span><span class="n">ledTask</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;Led&quot;</span><span class="p">,</span><span class="w"> </span><span class="mi">128</span><span class="p">,</span><span class="w"> </span><span class="nb">NULL</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="n">led_task</span><span class="p">);</span> <span class="w"> </span><span class="n">vTaskStartScheduler</span><span class="p">();</span> <span class="w"> </span><span class="c1">// Code will never reach here</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span> <span class="p">}</span> </pre></div> <p>Both the buttonTask and the ledTask function will seem to run in parallel and there's a few IPC systems to move data between the various tasks. The code above is not functional but I stripped it down to get the general usage across.</p> <p>I've used this for a few days to make an enormous mess of my codebase. I have created several tasks in my test project:</p> <ul><li>The buttonsTask that polls the i2c gpio expander to check if buttons have been pressed and then put a message on the button queue.</li> <li>The ledTask that sets the right RGB color on the right button by putting a message on the ledQueue;</li> <li>The mainTask that runs the main loop of the project that updates the state based on the button presses.</li> <li>The networkTask that communicates with the Wiznet module.</li> <li>The dhcpTask that is spawned by the networkTask when a network cable is plugged in.</li> <li>The mdnsTask that is spawned by the dhcpTask once an ip address is aquired.</li> <li>the atemTask that is spawned by the mdnsTask when it gets a response from an ATEM device.</li> <li>the viscaTask that does nothing but should send data out the RS-485 port.</li> </ul> <p>This is a lot of tasks and the hardware doesn't even do anything yet except appear on the network.</p> <p>I ran into a few issues with FreeRTOS. The main annoying one is that printf simply caused things to hang every single time which makes debugging very hard. Sure the gdb debugger works but it's not neat for dumping out DHCP traffic for example.</p> <p>The FreeRTOS also doesn't seem to provide any hardware abstraction at all which means all the code I wrote to communicate with the various chips is not easily re-used.</p> <p>After a few days I created a new clean FreeRTOS project and started porting the various functionalities from the previous version over to try to get a cleaner and more manageable codebase but ended up giving up because blind debugging because there's no serial output is quite annoying. I decided to look what the alternatives have to offer.</p> <h2>Apache NuttX</h2> <p>Another seemingly popular RTOS is NuttX. This project seems a lot closer to what you'd expect from a regular operating system. It makes your microcontroller look like an unix system.</p> <p>First thing the tutorial tells me to do is fetching the pico-sdk and set the environment variable. No problem, I already have the sdk in /usr/share and that environment variable already exists on my system. Suprisingly this made the build fail because NuttX decides that it really needs to overwrite the version.h file in my pico-sdk for which it doesn't have permissions... why...</p> <p>After doing the initial setup of building a minimal NuttX firmware for my board I connected to the serial port and was greeted by an actual shell.</p> <pre><code>nsh&gt; uptime 00:01:34 up 0:01, load average: 0.00, 0.00, 0.00 nsh&gt; uname NuttX nsh&gt; uname -a NuttX 12.5.1 9d6e2b97fb May 6 2024 15:18:54 arm raspberrypi-pico</code></pre> <p>It looks like I'd just be able to write an app for this operating system and have it auto-launch on boot. Since this tries to do the Unix thing it also has a filesystem of course so the hardware has FS abstractions like <code>/dev/i2c0</code> and <code>/dev/adc0</code>. </p> <p>One thing I liked a lot was that it's build around menuconfig/Kconfig which I'm already used to for Linux development. This also means there's an actual hardware driver system and the GPIO expander chip I've used for the buttons already had a driver. The menuconfig system also allows me to configure the pin muxing of the rp2040 chip so I don't have to keep constants around with pin numbers and do a bunch of hardware setup to make my i2c bus work. I can just go into the menuconfig and tell it that i2c0 of the pico is used and that it's on two specific pins. I've also enabled the i2c testing utility as one of the apps that will be build into the firmware.</p> <pre><code>nsh&gt; i2c dev 0 79 0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 70: -- -- -- -- -- -- -- -- -- -- nsh&gt; </code></pre> <p>Well uuuuh... yup the basics aren't working. I've spend a bit of time going through the rp2040 setup code and the various i2c related menuconfig options but it seems like this just doesn't really work...</p> <p><b>Update: This actually works fine but I managed to make a config mistake that broke the i2c bus, I have gotten the board to work more now on NuttX and will write a more detailed update post in the future for this. Consider the rest of this section as most likely invalid.</b></p> <p>I also have not figured out yet how I can tell NuttX that my gpio buttons are behind the gpio extender, or how to actually link the gpio extender to my non-functional i2c bus.</p> <p>Another thing that annoyed me is that I had to re-clone the nuttx repository multiple times simply because sometimes one of the configure.sh commands would fail which would leave the repository in an inconsistent state and the distclean command wouldn't work because the repository was in an inconsistent state. Really the classic "configure.sh: you are already configured; distclean: you are not configured yet"</p> <p>Unix-like seems great at first glance, but I don't really want to deal with filesystem paths on a microcontroller for a pretend filesystem. I also don't need a shell in my production system, it should just run my code.</p> <h2>Zephyr</h2> <p>So next on the list is Zephyr. This provides a python utility to set up a project which should make things a bit easier, or it's a sign something is terribly overcomplicated.</p> <p>The very first thing this project does is pull in 5GB of git repositories which includes the entire HAL library for every chip under the sun. The second thing it does is for some reason mess with my user-wide cmake stuff on my system.</p> <p>After that the tutorial told me to install the Zephyr SDK:</p> <blockquote>The <a href="https://docs.zephyrproject.org/latest/develop/toolchains/zephyr_sdk.html#toolchain-zephyr-sdk">Zephyr Software Development Kit (SDK)</a> contains toolchains for each of Zephyr’s supported architectures, which include a compiler, assembler, linker and other programs required to build Zephyr applications.<br><br>It also contains additional host tools, such as custom QEMU and OpenOCD builds that are used to emulate, flash and debug Zephyr applications.</blockquote> <p>Yeah no thanks, I have already several perfectly fine ARM toolchains and I don't really want to either build or fetch precompiled compilers for every architecture Zephyr supports, lets see if I can get away with not installing this.</p> <p>After some messing around I figured out how to get away with it. There need to be two command line options set for cross compiling:</p> <div class="highlight"><pre><span></span><span class="gp">$ </span><span class="nb">export</span><span class="w"> </span><span class="nv">ZEPHYR_TOOLCHAIN_VARIANT</span><span class="o">=</span>cross-compile <span class="gp">$ </span><span class="nb">export</span><span class="w"> </span><span class="nv">CROSS_COMPILE</span><span class="o">=</span>/usr/bin/arm-none-eabi-<span class="w"> </span> <span class="gp">$ </span>west<span class="w"> </span>build<span class="w"> </span>-p<span class="w"> </span>always<span class="w"> </span>-b<span class="w"> </span>sparkfun_pro_micro_rp2040<span class="w"> </span>samples/basic/blinky </pre></div> <p>One thing I also found out is that the Raspberry Pi Pico is not actually supported, only other boards that have the same SoC. No worries, these boards are practically the same. The very second issue I hit is that the blinky demo doesn't build because it requires <code>led0</code> to be defined to have something to blink.</p> <p>It turns out the Sparkfun pro Micro RP2040 does not actually have a simple gpio led to blink but a ws2812B adressable led. </p> <p>So I started following the custom board manual which told me to copy a random other board because that's how it always goes. Maybe if you already have a meta tool to set-up a project make it create this scaffolding.</p> <p>In the end I did not manage to build for my board because it simply wouldn't start to exist after fixing all the errors and warnings in the build.</p> <h2>Conclusion</h2> <p>Well at least with FreeRTOS I managed to building some of my own application. I guess I have to follow the online instructions of replacing printf with another printf implementation and make sure to call the different function everywhere.</p> <p>I'll probably continue on trying to get FreeRTOS to do the things I want since it's the only one that can be simply integrated in your own environment instead of the other way around.</p> Bootstrapping Alpine Linux without roothttps://blog.brixit.nl/bootstrapping-alpine-linux-without-root/98LinuxMartijn BraamWed, 20 Mar 2024 23:50:30 -0000<p>Creating a chroot in Linux is pretty easy: put a rootfs in a folder and run the <code>sudo chroot /my/folder</code> command. But what if you don't want to use superuser privileges for this?</p> <p>This is not super simple to fix, not only does the <code>chroot</code> command itself require root permissions but the steps for creating the rootfs in the first place and mounting the required filesystems like /proc and /sys require root as well.</p> <p>In pmbootstrap the process for creating an installable image for a phone requires setting up multiple chroots and executing many commands in those chroots. If you have the password timeout disabled in sudo you will notice that you will have to enter your password tens to hundreds of times depending on the operation you're doing. An example of this is shown in the long running "<a href="https://gitlab.com/postmarketOS/pmbootstrap/-/issues/2052#note_966447872">pmbootstrap requires sudo</a>" issue on Gitlab. In this example sudo was called 240 times!</p> <p>Now it is possible with a lot of refactoring to move batches of superuser-requiring commands into scripts and elevate the permissions of that with a single sudo call but to get this down to a single sudo call per pmbootstrap command would be really hard.</p> <h2>Another approach</h2> <p>So instead of building a chroot the "traditional" way what are the alternatives?</p> <p>The magic trick to get this working are user namespaces. From the Linux documentation:</p> <blockquote>User namespaces isolate security-related identifiers and attributes, in particular, user IDs and group IDs (see <a href="https://man7.org/linux/man-pages/man7/credentials.7.html">credentials(7)</a>), the root directory, keys (see <a href="https://man7.org/linux/man-pages/man7/keyrings.7.html">keyrings(7)</a>), and capabilities (see <a href="https://man7.org/linux/man-pages/man7/capabilities.7.html">capabilities(7)</a>). A process's user and group IDs can be different inside and outside a user namespace. In particular, a process can have a normal unprivileged user ID outside a user namespace while at the same time having a user ID of 0 inside the namespace; in other words, the process has full privileges for operations inside the user namespace, but is unprivileged for operations outside the namespace. </blockquote> <p>It basically allows running commands in a namespace where you have UID 0 on the inside without requiring to elevate any of the commands. This does have a lot of limitations though which I somehow all manage to hit with this.</p> <p>One of the tools that makes it relatively easy to work with the various namespaces in Linux is <code>unshare</code>. Conveniently this is also part of <code>util-linux</code> so it's a pretty clean dependency to have.</p> <h2>Building a rootfs</h2> <p>There's enough examples of using <code>unshare</code> to create a chroot without sudo but those all assume you already have a rootfs somewhere to chroot into. Creating the rootfs itself has a few difficulties already though.</p> <p>Since I'm building an Alpine Linux rootfs the utility I'm going to use is <code>apk.static</code>. This is a statically compiled version of the package manager in Alpine which allows building a new installation from an online repository. This is similar to <code>debootstrap</code> for example if you re more used to Debian than Alpine.</p> <p>There's a wiki page on running <a href="https://wiki.alpinelinux.org/wiki/Alpine_Linux_in_a_chroot">Alpine Linux in a chroot</a> that documents the steps required for setting up a chroot the traditional way with this. The initial commands to aquire the <code>apk.static</code> binary don't require superuser at all, but after that the problems start:</p> <div class="highlight"><pre><span></span><span class="gp">$ </span>./apk.static<span class="w"> </span>-X<span class="w"> </span><span class="si">${</span><span class="nv">mirror</span><span class="si">}</span>/latest-stable/main<span class="w"> </span>-U<span class="w"> </span>--allow-untrusted<span class="w"> </span>-p<span class="w"> </span><span class="si">${</span><span class="nv">chroot_dir</span><span class="si">}</span><span class="w"> </span>--initdb<span class="w"> </span>add<span class="w"> </span>alpine-base </pre></div> <p>This creates the Alpine installation in <code>${chroot_dir}</code>. This requires superuser privileges to set the correct permissions on the files of this new rootfs. After this there's two options of populating /dev inside this rootfs which both are problematic:</p> <div class="highlight"><pre><span></span><span class="gp">$ </span>mount<span class="w"> </span>-o<span class="w"> </span><span class="nb">bind</span><span class="w"> </span>/dev<span class="w"> </span><span class="si">${</span><span class="nv">chroot_dir</span><span class="si">}</span>/dev <span class="go">mounting requires superuser privileges and this exposes all your hardware in the chroot</span> <span class="gp">$ </span>mknod<span class="w"> </span>-m<span class="w"> </span><span class="m">666</span><span class="w"> </span><span class="si">${</span><span class="nv">chroot_dir</span><span class="si">}</span>/dev/full<span class="w"> </span>c<span class="w"> </span><span class="m">1</span><span class="w"> </span><span class="m">7</span> <span class="gp">$ </span>mknod<span class="w"> </span>-m<span class="w"> </span><span class="m">644</span><span class="w"> </span><span class="si">${</span><span class="nv">chroot_dir</span><span class="si">}</span>/dev/random<span class="w"> </span>c<span class="w"> </span><span class="m">1</span><span class="w"> </span><span class="m">8</span> <span class="go">... etcetera, the mknod command also requires superuser privileges</span> </pre></div> <p>The steps after this have similar issues, most of them for <code>mount</code> reasons or <code>chown</code> reasons.</p> <p>There is a few namespace options from <code>unshare</code> used to work around these issues. The command used to run <code>apk.static</code> in my test implementation is this:</p> <div class="highlight"><pre><span></span><span class="gp">$ </span>unshare<span class="w"> </span><span class="se">\</span> <span class="w"> </span>--user<span class="w"> </span><span class="se">\</span> <span class="w"> </span>--map-users<span class="o">=</span><span class="m">10000</span>,0,10000<span class="w"> </span><span class="se">\</span> <span class="w"> </span>--map-groups<span class="o">=</span><span class="m">10000</span>,0,10000<span class="w"> </span><span class="se">\</span> <span class="w"> </span>--setuid<span class="w"> </span><span class="m">0</span><span class="w"> </span><span class="se">\</span> <span class="w"> </span>--setgid<span class="w"> </span><span class="m">0</span><span class="w"> </span><span class="se">\</span> <span class="w"> </span>--wd<span class="w"> </span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">chroot_dir</span><span class="si">}</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span> <span class="w"> </span>./apk-tools-static<span class="w"> </span>-X...etc </pre></div> <p>This will use <code>unshare</code> to create a new userns and change the uid/gid inside that to 0. This effectively grants root privileges inside this namespace. But that's not enough.</p> <p>If <code>chown</code> is used inside the namespace it will still fail because my unprivileged user still can't change the permissions of those files. The solution to that is the uid remapping with <code>--map-users</code> and <code>--map-groups</code>. In the example above it sets up the namespace so files created with uid 0 will generate files with the uid 100000 on the actual filesystem. uid 1 becomes 100001 and this continues on for 10000 uids. </p> <p>This again does not completely solve the issue though because my unprivileged user still can't chown those files, doesn't matter if it's chowning to uid 0 or 100000. To give my unprivileged user this permission the <code>/etc/subuid</code> and <code>/etc/subgid</code> files on the host system have to be modified to add a rule. This sadly requires root privileges <i>once</i> to set up this privilege. To make the command above work I had to add this line to those two files:</p> <pre><code>martijn:100000:10000</code></pre> <p>This grants the user with the name <code>martijn</code> the permission to use 10.000 uids starting at uid 100.000 for the purpose of userns mapping.</p> <p>The result of this is that the <code>apk.static</code> command will seem to Just Work(tm) and the resulting files in <code>${chroot_dir}</code> will have all the right permissions but only offset by 100.000.</p> <h2>One more catch</h2> <p>There is one more complication with remapped uids and <code>unshare</code> that I've skipped over in the above example to make it clearer, but the command inside the namespace most likely cannot start.</p> <p>If you remap the uid with <code>unshare</code> you get more freedom inside the namespace, but it limits your privileges outside the namespace even further. It's most likely that the <code>unshare</code> command above was run somewhere in your own home directory. After changing your uid to 0 inside the namespace your privilege to the outside world will be as if you're uid 100.000 and that uid most likely does not have privileges. If any of the folders in the path to the executable you want <code>unshare</code> to run for you inside the namespace don't have the read and execute bit set for the "other" group in the unix permissions then the command will simply fail with "Permission denied".</p> <p>The workaround used in my test implementation is to just first copy the executable over to <code>/tmp</code> and hope you at least still have permissions to read there.</p> <h2>Completing the rootfs</h2> <p>So after all that the first command from the Alpine guide is done. Now there's only the problems left for mounting filesystems and creating files.</p> <p>While <code>/etc/subuid</code> does give permission to use a range of uids as an unprivileged user with a user namespace it does not give you permissions to create those files outside the namespace. So the way those files are created is basically the complicated version of <code>echo "value" | sudo tee /root/file</code>: </p> <div class="highlight"><pre><span></span><span class="gp">$ </span><span class="nb">echo</span><span class="w"> </span><span class="s2">&quot;nameserver a.b.c.d&quot;</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>unshare<span class="w"> </span><span class="se">\</span> <span class="w"> </span>--user<span class="w"> </span><span class="se">\</span> <span class="w"> </span>--map-users<span class="o">=</span><span class="m">10000</span>,0,10000<span class="w"> </span><span class="se">\</span> <span class="w"> </span>--map-groups<span class="o">=</span><span class="m">10000</span>,0,10000<span class="w"> </span><span class="se">\</span> <span class="w"> </span>--setuid<span class="w"> </span><span class="m">0</span><span class="w"> </span><span class="se">\</span> <span class="w"> </span>--setgid<span class="w"> </span><span class="m">0</span><span class="w"> </span><span class="se">\</span> <span class="w"> </span>--wd<span class="w"> </span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">chroot_dir</span><span class="si">}</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span> <span class="w"> </span>sh<span class="w"> </span>-c<span class="w"> </span><span class="s1">&#39;cat &gt; /etc/resolv.conf&#39;</span> </pre></div> <p>This does set-up and tear down the entire namespace for every file change or creation which is a bit inefficient, but inefficient is still better than impossible. Changing file permissions is done in a similar way.</p> <p>To fix the mounting issue there's the mount namespace functionality in Linux. This allows creating new mounts inside the namespace as long as you still have permissions on the source file as your unprivileged user. This effectively means you can't use this to mount random block devices but it works great for things like <code>/proc</code> and loop mounts.</p> <p>There is a <code>--mount-proc</code> parameter that will tell <code>unshare</code> to set-up a mount namespace and then mount <code>/proc</code> inside the namespace at the right place so that's what I'm using. But I still need other things mounted. This mounting is done as a small inline shell script right before executing the commands inside the chroot:</p> <div class="highlight"><pre><span></span><span class="gp">$ </span>unshare<span class="w"> </span><span class="se">\</span> <span class="w"> </span>--user<span class="w"> </span><span class="se">\</span> <span class="w"> </span>--fork<span class="w"> </span><span class="se">\</span> <span class="w"> </span>--pid<span class="w"> </span><span class="se">\</span> <span class="w"> </span>--mount<span class="w"> </span><span class="se">\</span> <span class="w"> </span>--mount-proc<span class="w"> </span><span class="se">\</span> <span class="w"> </span>--map-users<span class="o">=</span><span class="m">10000</span>,0,10000<span class="w"> </span><span class="se">\</span> <span class="w"> </span>--map-groups<span class="o">=</span><span class="m">10000</span>,0,10000<span class="w"> </span><span class="se">\</span> <span class="w"> </span>--setuid<span class="w"> </span><span class="m">0</span><span class="w"> </span><span class="se">\</span> <span class="w"> </span>--setgid<span class="w"> </span><span class="m">0</span><span class="w"> </span><span class="se">\</span> <span class="w"> </span>--wd<span class="w"> </span><span class="s2">&quot;</span><span class="si">${</span><span class="nv">chroot_dir</span><span class="si">}</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span> <span class="w"> </span>--<span class="w"> </span><span class="se">\</span> <span class="w"> </span>sh<span class="w"> </span>-c<span class="w"> </span><span class="s2">&quot; \</span> <span class="s2"> mount -t proc none proc ; \</span> <span class="s2"> touch dev/zero ; \</span> <span class="s2"> mount -o rw,bind /dev/zero dev/zero ;\</span> <span class="s2"> touch dev/null ; \</span> <span class="s2"> mount -o row,bind /dev/null dev/null ;\</span> <span class="s2"> ...</span> <span class="go"> chroot . bin/sh \</span> <span class="go"> &quot;</span> </pre></div> <p>The mounts are created right between setting up the namespaces but before the chroot is started so the host filesystem can still be accessed. The working directory is set to the root of the rootfs using the <code>--wd</code> parameter of <code>unshare</code> and then bind mounts are made from <code>/dev/zero</code> to <code>dev/zero</code> to create those devices inside the rootfs.</p> <p>This combines the two impossible options to make it work. <code>mknod</code> can still not work inside namespaces because it is a bit of a security risk. <code>mount</code>'ing /dev gives access to way too many devices that are not needed but the mount namespace does allow bind-mounting the existing device nodes one by one and allows me to filter them.</p> <p>Then finally... the <code>chroot</code> command to complete the journey. This has to refer to the rootfs with a relative path and this also depends on the working directory being set by <code>unshare</code> since host paths are breaking with uid remapping.</p> <h2>What's next?</h2> <p>So this creates a full chroot without superuser privileges (after the initial setup) and this whole setup even works perfectly with having cross-architecture chroots in combination with <code>binfmt_misc</code>. </p> <p>Compared to <code>pmbootstrap</code> this codebase does very little and there's more problems to solve. For one all the filesystem manipulation has to be figured out to copy the contents of the chroot into a filesystem image that can be flashed. This is further complicated by the mangling of the uids in the host filesystem so it has to be remapped while writing into the filesystem again.</p> <p>Flashing the image to a fastboot capable device should be pretty easy without root privileges, it only requires an udev rule that is usually already installed by the android-tools package on various Linux distributions. For the PinePhone flashing happens on a mass-storage device and as far as I know it will be impossible to write to that without requiring actual superuser privileges.</p> <p>The code for this is in the <a href="https://git.sr.ht/~martijnbraam/ambootstrap">~martijnbraam/ambootstrap</a> repository, hopefully in some time I get this to actually write a plain Alpine Linux image to a phone :D</p> <p></p>