Bridgetown2024-03-18T10:36:45+02:00https://www.randomerrata.com/feed.xmlRandom ErrataMy ramblings about technology, programming, and whatever pops into my headOn-demand image resizing with Bridgetown2024-03-09T12:25:00+02:002024-03-09T12:25:00+02:00https://www.randomerrata.com/articles/2024/bridgetown-image-resizing/<p>As I recently migrated this blog from <a href="https://middlemanapp.com">Middleman</a> to <a href="https://www.bridgetownrb.com">Bridgetown</a>, I thought I could document one of my Bridgetown customizations: on-demand or build-time image resizing.</p>
<h2 id="notes">Notes</h2>
<ul>
<li>In this approach, the originals are stored in the <code class="highlighter-rouge">frontend/</code> directory and should therefore be also accessible via the Bridgetown built-in <code class="highlighter-rouge">asset_path</code> helper or through esbuild imports.</li>
<li>The resized images are stored in <code class="highlighter-rouge">src/images/resized</code> so that they don’t need to be processed by esbuild</li>
<li>The resized images contain a digest of the original (CRC32 by default) ensuring that they get reprocessed if the original changes.
<ul>
<li>The CRC hash should be sufficient for detecting changes in the original file in a non-adversarial environment, and it is <em>very</em> <a href="/articles/2024/hashing-benchmarks/">fast</a> to compute.</li>
<li>The CRC code can be easily replaced with a more robust, though slower, cryptographic hash function if desired</li>
</ul>
</li>
<li>On the other hand, I’ve chosen not to add a hash of the generated image to the filename in order to avoid having to keep track of the originals and generated files in a manifest file.</li>
<li>I’ve chosen not to commit the generated files to Git, but you might make a different choice based on your needs. If you do push the resized images to version control, make sure to also clean up any versions that are no longer needed.</li>
</ul>
<!-- READMORE -->
<h2 id="prerequisites-and-assumptions">Prerequisites and assumptions</h2>
<p>For this tutorial, I’m assuming the following:</p>
<ul>
<li>Bridgetown v1.3</li>
<li>Ruby 3.3</li>
<li>ERB templates instead of Liquid
<ul>
<li>ERB is going to be <a href="https://www.bridgetownrb.com/future/road-to-bridgetown-2.0-new-baselines/">the default in Bridgetown 2.0</a>
</li>
<li>I prefer ERB to Liquid in any case</li>
<li>It should be possible to <a href="https://www.bridgetownrb.com/docs/plugins/filters">adapt these steps for Liquid templates</a>, but I haven’t explored those details</li>
</ul>
</li>
<li>You’re somewhat familiar with Ruby and Bridgetown already</li>
<li>You should have <a href="https://www.libvips.org">libvips</a> installed
<ul>
<li>macOS: <code class="highlighter-rouge">brew install vips</code>
</li>
<li>Debian/Ubuntu: <code class="highlighter-rouge">sudo apt install libvips</code>
</li>
</ul>
</li>
</ul>
<h2 id="steps">Steps</h2>
<ol>
<li>
<p>If you don’t have a Bridgetown site yet, create one:</p>
<div class="language-sh highlighter-rouge">
<div class="highlight"><pre class="highlight"><code>bridgetown new <span class="nt">--templates</span><span class="o">=</span>erb image-resizing-demo
<span class="nb">cd </span>image-resizing-demo
</code></pre></div> </div>
</li>
<li>
<p>Add the <a href="https://github.com/janko/image_processing">ImageProcessing</a> gem to the <code class="highlighter-rouge">Gemfile</code>:</p>
<div class="language-ruby highlighter-rouge">
<div class="highlight"><pre class="highlight"><code><span class="n">gem</span> <span class="s1">'image_processing'</span><span class="p">,</span> <span class="s1">'~> 1.12'</span><span class="p">,</span> <span class="s1">'>= 1.12.2'</span>
</code></pre></div> </div>
</li>
<li>
<p>Add a test image to <code class="highlighter-rouge">frontend/images</code>:</p>
<div class="language-sh highlighter-rouge">
<div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> frontend/images
<span class="c"># You can, of course, use any image you like as your test subject…</span>
curl <span class="nt">--output</span> frontend/images/blue-marble.jpg <span class="se">\</span>
https://www.randomerrata.com/_bridgetown/static/blue-marble-T6POMCUA.jpg
</code></pre></div> </div>
</li>
<li>
<p>Create <code class="highlighter-rouge">plugins/builders/asset_helpers.rb</code>:</p>
<div class="language-ruby highlighter-rouge">
<div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s2">"image_processing/vips"</span>
<span class="nb">require</span> <span class="s2">"zlib"</span>
<span class="c1"># If you want to use a cryptographic digest, you can uncomment following:</span>
<span class="c1">#</span>
<span class="c1"># require "digest/sha2"</span>
<span class="k">class</span> <span class="nc">Builders::AssetHelpers</span> <span class="o"><</span> <span class="no">SiteBuilder</span>
<span class="k">def</span> <span class="nf">build</span>
<span class="n">helper</span> <span class="ss">:resized_image_path</span> <span class="k">do</span> <span class="o">|</span><span class="n">src</span><span class="p">,</span> <span class="ss">width: </span><span class="kp">nil</span><span class="p">,</span> <span class="ss">height: </span><span class="kp">nil</span><span class="o">|</span>
<span class="n">basename</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">basename</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="s2">".*"</span><span class="p">)</span>
<span class="n">ext</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">extname</span><span class="p">(</span><span class="n">src</span><span class="p">)</span>
<span class="c1"># Find images from the frontend directory</span>
<span class="n">frontend_path</span> <span class="o">=</span> <span class="no">Pathname</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">site</span><span class="p">.</span><span class="nf">root_dir</span><span class="p">).</span><span class="nf">join</span><span class="p">(</span><span class="s2">"frontend"</span><span class="p">)</span>
<span class="n">image_path</span> <span class="o">=</span> <span class="n">frontend_path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">src</span><span class="p">)</span>
<span class="c1"># Calculate a digest based on the original image file</span>
<span class="c1">#</span>
<span class="n">digest</span> <span class="o">=</span> <span class="no">Zlib</span><span class="p">.</span><span class="nf">crc32</span><span class="p">(</span><span class="no">File</span><span class="p">.</span><span class="nf">read</span><span class="p">(</span><span class="n">image_path</span><span class="p">)).</span><span class="nf">to_s</span><span class="p">(</span><span class="mi">16</span><span class="p">)</span>
<span class="c1">#</span>
<span class="c1"># It's also possible to use a cryptographically secure digest but</span>
<span class="c1"># calculating it will be slower and it's not necessarily necessary for</span>
<span class="c1"># this use case.</span>
<span class="c1">#</span>
<span class="c1"># To use SHA256, uncomment the following and remove the CRC digest line:</span>
<span class="c1">#</span>
<span class="c1"># digest = Digest::SHA256.file(image_path).hexdigest</span>
<span class="c1">#</span>
<span class="c1"># Put resized images inside the src/images/resized directory</span>
<span class="n">static_images_dir</span> <span class="o">=</span> <span class="no">Pathname</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">site</span><span class="p">.</span><span class="nf">root_dir</span><span class="p">).</span><span class="nf">join</span><span class="p">(</span><span class="s2">"src"</span><span class="p">,</span> <span class="s2">"images"</span><span class="p">)</span>
<span class="n">destination_dir</span> <span class="o">=</span> <span class="no">Pathname</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">static_images_dir</span><span class="p">).</span><span class="nf">join</span><span class="p">(</span><span class="s2">"resized"</span><span class="p">)</span>
<span class="c1"># Generate the resized filename based on the original filename, digest, and dimensions</span>
<span class="n">resized_filename</span> <span class="o">=</span> <span class="s2">"</span><span class="si">#{</span><span class="n">basename</span><span class="si">}</span><span class="s2">-</span><span class="si">#{</span><span class="n">digest</span><span class="si">}</span><span class="s2">-</span><span class="si">#{</span><span class="n">width</span><span class="si">}</span><span class="s2">x</span><span class="si">#{</span><span class="n">height</span><span class="si">}#{</span><span class="n">ext</span><span class="si">}</span><span class="s2">"</span>
<span class="n">destination_path</span> <span class="o">=</span> <span class="n">destination_dir</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">resized_filename</span><span class="p">)</span>
<span class="c1"># Don't do any processing if the file already exists</span>
<span class="k">if</span> <span class="o">!</span><span class="n">destination_path</span><span class="p">.</span><span class="nf">exist?</span>
<span class="no">Bridgetown</span><span class="p">.</span><span class="nf">logger</span><span class="p">.</span><span class="nf">info</span> <span class="s2">"Resizing image: </span><span class="si">#{</span><span class="n">src</span><span class="si">}</span><span class="s2"> to </span><span class="si">#{</span><span class="n">width</span><span class="si">}</span><span class="s2">×</span><span class="si">#{</span><span class="n">height</span><span class="si">}</span><span class="s2">"</span>
<span class="no">FileUtils</span><span class="p">.</span><span class="nf">mkdir_p</span><span class="p">(</span><span class="n">destination_dir</span><span class="p">)</span>
<span class="n">pipeline</span> <span class="o">=</span> <span class="no">ImageProcessing</span><span class="o">::</span><span class="no">Vips</span><span class="p">.</span><span class="nf">source</span><span class="p">(</span><span class="n">image_path</span><span class="p">).</span><span class="nf">resize_to_limit</span><span class="p">(</span><span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">)</span>
<span class="n">pipeline</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="ss">destination: </span><span class="n">destination_path</span><span class="p">.</span><span class="nf">to_s</span><span class="p">)</span>
<span class="k">else</span>
<span class="no">Bridgetown</span><span class="p">.</span><span class="nf">logger</span><span class="p">.</span><span class="nf">debug</span> <span class="s2">"Resized image exists: </span><span class="si">#{</span><span class="n">destination_path</span><span class="si">}</span><span class="s2">"</span>
<span class="k">end</span>
<span class="c1"># Calculate the relative path to the resized image in the src/images folder</span>
<span class="n">resized_path</span> <span class="o">=</span> <span class="n">destination_path</span><span class="p">.</span><span class="nf">relative_path_from</span><span class="p">(</span><span class="n">static_images_dir</span><span class="p">)</span>
<span class="c1"># Make sure that Bridgetown knows about the resized image</span>
<span class="n">site</span><span class="p">.</span><span class="nf">static_files</span> <span class="o"><<</span> <span class="o">::</span><span class="no">Bridgetown</span><span class="o">::</span><span class="no">StaticFile</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span>
<span class="n">site</span><span class="p">,</span>
<span class="n">site</span><span class="p">.</span><span class="nf">source</span><span class="p">,</span>
<span class="s2">"images"</span><span class="p">,</span>
<span class="n">resized_path</span>
<span class="p">)</span>
<span class="c1"># Return the path to the resized image</span>
<span class="s2">"/images/</span><span class="si">#{</span><span class="n">resized_path</span><span class="si">}</span><span class="s2">"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div> </div>
</li>
<li>
<p>Add the test image to <code class="highlighter-rouge">src/index.md</code>:</p>
<div class="language-md highlighter-rouge">
<div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">layout</span><span class="pi">:</span> <span class="s">default</span>
<span class="nn">---</span>
<span class="gh"># Welcome to your new Bridgetown website.</span>
<span class="p">![</span><span class="nv">Blue Marble</span><span class="p">](</span><span class="nt"><</span><span class="sx">%= resized_image_path("images/blue-marble.jpg", width: 1024) %</span><span class="nt">></span><span class="p">)</span>
... the rest of the file continues
</code></pre></div> </div>
</li>
<li>
<p>Ignore the generated images in Git by adding <code class="highlighter-rouge">resized</code> directory to <code class="highlighter-rouge">.gitignore</code> (optional):</p>
<div class="language-sh highlighter-rouge">
<div class="highlight"><pre class="highlight"><code><span class="c"># Ignore generated images</span>
/src/images/resized
</code></pre></div> </div>
</li>
<li>
<p>Start the Bridgetown server: <code class="highlighter-rouge">bin/bridgetown start</code></p>
</li>
<li>
<p>You should see the resized image when you visit <a href="http://localhost:4000">http://localhost:4000</a></p>
</li>
</ol>
<h2 id="screenshot">Screenshot</h2>
<p><img src="/images/resized/2024-03-09-bridgetown-image-resizing/screenshot-f110ada8-1720x.png" alt="On-demand image resizing demo"></p>Non-scientific hash benchmarks2024-03-06T22:00:00+02:002024-03-06T22:00:00+02:00https://www.randomerrata.com/articles/2024/hashing-benchmarks/<p>Which Ruby hash or message digest algorithm is the speediest in Ruby? My cursory Googling didn’t surface an up-to-date benchmark from anyone else so here are some rough results of my own.</p>
<p><strong>Note:</strong> These algorithms are not all equivalent so don’t just blindly pick the fastest one! CRC32, MurmurHash, xxHash, and CityHash are all non-cryptographic.</p>
<ul>
<li>Ruby stdlib:
<ul>
<li><a href="https://docs.ruby-lang.org/en/3.3/Digest/MD5.html">MD5</a></li>
<li>
<a href="https://docs.ruby-lang.org/en/3.3/Digest/SHA1.html">SHA1</a>/<a href="https://docs.ruby-lang.org/en/3.3/Digest/SHA2.html">SHA256</a>/<a href="https://docs.ruby-lang.org/en/3.3/Digest/SHA2.html">SHA512</a>
</li>
<li><a href="https://docs.ruby-lang.org/en/3.3/Zlib.html#method-c-crc32">Zlib CRC32</a></li>
</ul>
</li>
<li>Gems:
<ul>
<li>
<a href="https://rubygems.org/gems/digest-xxhash">digest-xxhash</a> v0.2.7 (XXH32/XXH64/XXH3)</li>
<li>
<a href="https://rubygems.org/gems/murmurhash3">murmurhash3</a> v0.1.7 (32/128)</li>
<li>
<a href="https://rubygems.org/gems/cityhash">cityhash</a> v0.9.0 (32/64/128)</li>
<li>
<a href="https://rubygems.org/gems/openssl">openssl</a> v3.2.0 (MD5, SHA1, SHA256, SHA512, SHA3)</li>
</ul>
</li>
</ul>
<p><strong>Update 2024-03-11:</strong> Added OpenSSL::Digest for MD5, SHA1, SHA256, SHA512, and SHA3</p>
<h2 id="results">Results</h2>
<p>100,000 iterations of hashing a 1 MiB blob of random data.</p>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="/_bridgetown/static/chart-dark-W6WE2UMD.svg"></source>
<source media="(prefers-color-scheme: light)" srcset="/_bridgetown/static/chart-light-SYK4GF5U.svg"></source>
<img alt="Chart of benchmark results" src="/_bridgetown/static/chart-light-SYK4GF5U.svg">
</picture>
<p>The benchmarks were run on an M1 MacBook Pro.</p>
<ul>
<li>Apple M1 Pro with 10 CPU cores (8 performance and 2 efficiency)</li>
<li>macOS Sonoma 14.3.1</li>
<li>Ruby 3.3.0</li>
<li>OpenSSL 3.2.1</li>
</ul>
<p>The benchmark source code is available on <a href="https://github.com/matiaskorhonen/digest_bench">GitHub</a>.</p>Simple syrup calculator2023-11-28T20:00:00+02:002023-11-28T20:00:00+02:00https://www.randomerrata.com/articles/2023/simple-syrup/<p>Simple syrup is an extremely common ingredient in cocktails and, as the name implies, very simple to make: it’s just sugar and water in a 1:1 ratio. What’s slightly less easy is knowing how much water and sugar to measure out to get a given volume of syrup.</p>
<p>My calculator uses the densities of water (1g/ml) and crystalline sucrose (<a href="http://www.sugartech.co.za/density/index.php">1.5862g/ml</a>) to do a fairly naïve estimation of the final volume.</p>
<p>This isn’t totally accurate because densities don’t quite work that way in real life. Possibly the most well known example of this is what happens when you mix ethanol and water (<a href="https://sciencedemonstrations.fas.harvard.edu/presentations/mixing-ethanol-and-water">250ml + 250ml = 480ml</a>).</p>
<p>However, for cocktails this gets us in the ballpark and the small difference doesn’t really matter.</p>
<div class="glitch-embed-wrap" style="height: 340px; width: 100%; margin-bottom: 1rem;">
<iframe src="https://simple-syrup-calculator.glitch.me/" loading="lazy" title="Simple syrup calculator on Glitch" allow="fullscreen" allowfullscreen="" style="height: 100%; width: 100%; border: 0; border-radius: 0.375rem;">
</iframe>
</div>
<h2 id="recipe">Recipe</h2>
<p>Combine both ingredients in a saucepan and cook over medium heat until all the sugar has dissolved.</p>
<p>Transfer immediately to a heat-safe bottle or let it cool to room temperature in the saucepan and then transfer to a bottle.</p>
<p>Simple syrup should be refrigerated unless consumed within a day or so. The sugar content is too low to prevent the growth of microbes.</p>PaperAge: Easy and secure paper backups of secrets2023-06-07T11:55:00+03:002023-06-07T11:55:00+03:00https://www.randomerrata.com/articles/2023/paperage/<figure>
<img src="/_bridgetown/static/paper-age-social-IZPBLROJ.jpg" alt="PaperAge: Easy and secure paper backups of secrets" loading="lazy" data-src="/_bridgetown/static/paper-age-social-IZPBLROJ.jpg" data-blurhash="LbLW@RxuDNs-oct7ozj[RPj@bcay" width="2048" height="1024">
</figure>
<p>My first Rust-based project, <a href="https://github.com/matiaskorhonen/paper-age">PaperAge</a>, is a solution for making secure paper backups of important secrets. The backups are secured with state-of-the-art cryptography using the <a href="https://age-encryption.org/v1">age format</a>.</p>
<p>Personally, I use it to make paper backups of the bare minimum credentials I’d need to regain access to all my online accounts and backups. For example, the 1Password recovery kit plus my email account and Apple ID credentials.</p>
<p>One of the requirements for PaperAge was being able to print the backups at any printer without having to trust the printer or whoever operates it. This is why the generated PDF has a blank space for the passphrase. Additionally, this allows you to store the passphrase separately for extra security (either physically or you can trust yourself to remember it).</p>
<p>My personal threat model doesn’t include state actors or extremely tech-savvy burglars, so just relying on physical security and/or obscurity provides a sufficient level of security for my needs. I’m mostly guarding against the unlikely event where I lose access to all my devices at the same time and have to regain access to my accounts and backups from scratch.</p>
<p>Equally, I didn’t want the backup solution to be dependent on any one tool or person. Not even if it was my own project. To this end, you don’t need PaperAge to recover from backups made with it. Instead, you can use either the original <a href="https://github.com/FiloSottile/age">age</a> CLI tool or any <a href="https://github.com/FiloSottile/awesome-age">compatible implementation</a>. Behind the scenes, PaperAge uses <a href="https://github.com/str4d/rage">rage</a>, the Rust port of age.</p>
<h2 id="automated-releases">Automated releases</h2>
<p><img src="/_bridgetown/static/mermaid-diagram-WMFWIXDL.svg" alt="Diagram showing the GitHub actions for releasing PaperAge"></p>
<p>The release workflow for PaperAge is highly automated and uses <a href="https://github.com/crate-ci/cargo-release">cargo release</a> to tag and publish new releases.</p>
<p>Pushing a release tag to GitHub kicks off a GitHub Actions workflow that will create a new draft release on GitHub, cross compile for macOS, Linux, and Windows, and publish the release once compiled. Finally, it sends a <code class="highlighter-rouge">workflow_dispatch</code> event to the <a href="https://github.com/matiaskorhonen/homebrew-paper-age">matiaskorhonen/homebrew-paper-age</a> repository to kick off a GitHub Action that updates the Homebrew formula for PaperAge.</p>
<p>The Homebrew formula <a href="https://github.com/matiaskorhonen/homebrew-paper-age/blob/main/.github/workflows/formula.yml">update action</a> will install Homebrew, update the formula based on a template, test that the formula still works, and finally commit and push the updated formula.</p>
<p>Eventually, I’m planning on trying to get a PaperAge formula up-streamed to Homebrew itself, but the current Cask workflow works so well that I haven’t been very motivated to get it done so far.</p>
<h2 id="further-reading">Further reading</h2>
<ul>
<li>
<a href="https://github.com/FiloSottile/awesome-age">Awesome Age</a>: A collection of projects and resources in the age file encryption ecosystem</li>
<li>
<a href="https://github.com/crate-ci/cargo-release">cargo release</a>: Cargo subcommand for releasing a crate</li>
<li>
<a href="https://github.com/taiki-e/upload-rust-binary-action">upload-rust-binary-action</a>: GitHub Action for building and uploading Rust binary to GitHub Releases</li>
</ul>Reflections on Euruko 20222023-06-06T10:00:00+03:002023-06-06T10:00:00+03:00https://www.randomerrata.com/articles/2023/reflections/<p>Last year finally brought to a close my three-year journey as an Euruko organiser. Euruko Helsinki took place at last, in-person, on the 13th and 14th of October 2022. 640 wonderful Rubyists from all over Europe and beyond descended on Helsinki, and we managed to have a wonderful in-person Euruko for the first time since 2019. Plus, another 200 or so joined the conference remotely.</p>
<p><img src="/_bridgetown/static/img-00349-RIWAXW4I.jpg" alt="Friday hug at Euruko 2023"></p>
<p>Many people might not realise just how weird Euruko is on the conference circuit.</p>
<ol>
<li>The organisers change <em>completely</em> every year. This means that there is very little institutional knowledge about organising the event. Every organiser ends up having to make their own decisions about everything from ticket and sponsorship pricing to venues to finances and taxation.</li>
<li>The country changes. I’m perfectly happy to lend a hand to the organisers of next year’s Euruko in Vilnius, but I have no knowledge whatsoever about venues, laws, or vendors in Lithuania, so my experience from Euruko 2022 will be of limited use. We help where we can, of course, but we have our own events to plan too.</li>
</ol>
<p>I’m aware of no other event that operates like this, certainly not at this scale. Setting up an event for 500–700 people is a lot of work and involves a lot of moving pieces. Depending on your ticket prices and sponsorship sales, there’s quite a lot of money at stake. For example, the venue and catering alone cost us north of €80K.</p>
<p>As far as ticket and sponsorship sales go, we were able to take advantage of some pent-up demand in the community, so there wasn’t any doubt that the conference would be financially viable. At least not after we’d cleared the hurdle of the 2020 postponement and the 2021 remote-only conference. Even so, 1–2 months out from the conference, it looked like the event wouldn’t sell out. It did in the end, but much closer to the event dates than we would have liked. One of the biggest favours you can do for an event organiser is to buy your ticket as early as possible.</p>
<p>Euruko 2022 was run with an extremely lean organisation with most of the work going to me and <a href="https://mastodon.social/@simovirtanen">Simo Virtanen</a> with assistance from other volunteers as needed. For example, we had a small team of people for talk proposal reviews and a handful of volunteers for the conference days. It would have been a smart move to recruit volunteers earlier and to get them more involved sooner in order to offload some of the workload from my shoulders.</p>
<p>In the end, the event ran just about as well as I could have hoped, and the feedback from participants was overwhelmingly positive. I’m happy we had the chance to bring so many Rubyists together in Helsinki, but I’m not in a rush to organise another event of the same scale again. 😅</p>
<p><em>PS. I intended to write this blog post a month or two after Euruko 2022 but, as I often do with blogging, I procrastinated. Better late than never, I suppose.</em></p>
<p><em>PPS. If you liked Euruko 2022, have a look at our next conference, <a href="https://oh.helsinkiruby.fi">Oh the Humanity!</a></em></p>
<p><strong>Related post:</strong> <a href="/articles/2021/euruko-in-a-pandemic/">Picking the worst year to pitch for Euruko</a></p>Photo Sphere2022-06-29T12:19:00+03:002022-06-29T12:19:00+03:00https://www.randomerrata.com/articles/2022/photo-sphere/<figure>
<img data-photosphere="/_bridgetown/static/Photo_6553623_DJI_23_pano_43361576_0_202262414530_photo_pano-OXN47K7B.jpg" data-resized="/images/resized/2022-06-29-photo-sphere/Photo_6553623_DJI_23_pano_43361576_0_202262414530_photo_pano-1ade8621-1200x.jpg" class="photosphere" alt="Open in a browser to view the photo sphere" title="Open in a browser to view the photo sphere" src="/images/resized/2022-06-29-photo-sphere/Photo_6553623_DJI_23_pano_43361576_0_202262414530_photo_pano-1ade8621-1200x.jpg">
<figcaption>
<p>360° view of the Finnish countryside near Nastola</p>
</figcaption>
</figure>WWDC22 wishlist2022-06-01T14:19:00+03:002022-06-01T14:19:00+03:00https://www.randomerrata.com/articles/2022/dubdubtwotwo/<p>In no particular order and not particularly well organised.</p>
<h2 id="ios-16">iOS 16</h2>
<ul>
<li>A more comprehensive Map component in SwiftUI (support clustering, custom layers and so forth)</li>
<li>A way to persist list expansion state for deeply nested Swift UI lists</li>
<li>TranslationKit (allow apps to use Apple’s translation service instead of needing to send users to Google Translate)</li>
<li>Better picture and video quality in shared albums (currently limited to <a href="https://support.apple.com/en-us/HT202299">2048px on the long edge and 720p</a>, respectively)</li>
<li>Fix the autocorrect mess</li>
<li>Walking instructions in public transportation navigation. In other words, give me turn-by-turn instructions if I need to walk from the last stop to my destination.</li>
</ul>
<h2 id="macos-13">macOS 13</h2>
<ul>
<li>Native collapsing of menu bar items in macOS (please Sherlock Bartender)</li>
</ul>
<h2 id="xcode">Xcode</h2>
<ul>
<li>Code autoformat for Swift & Objective-C (in the vein of <a href="https://go.dev/blog/gofmt">gofmt</a> or <a href="https://prettier.io">prettier</a>)</li>
<li>More reliable SwiftUI previews</li>
</ul>
<h2 id="moonshots">Moonshots</h2>
<ul>
<li>A Music app that isn’t terrible (both on iOS and macOS)</li>
<li>A redesigned Home app</li>
<li>Having an Apple Watch shouldn’t block iOS app builds if they don’t have a watchOS build target (“Preparing the watch for development”)</li>
</ul>Awair PM2.5 sensor repair2022-04-23T13:37:00+03:002022-04-23T13:37:00+03:00https://www.randomerrata.com/articles/2022/awair-repair/<figure>
<img src="/_bridgetown/static/IMG_2386-I4DNHGHU.jpg" alt="Awair PM2.5 sensor repair" loading="lazy" data-src="/_bridgetown/static/IMG_2386-I4DNHGHU.jpg" data-blurhash="LMNK3NkE~qRiROIUNG%M^+ofM|R*" width="1720" height="1075">
</figure>
<p>Recently the fan on the PM2.5 sensor on my Awair had started to make an annoying amount of noise and no amount of canned air seemed to fix it. As I didn’t want to spend €329 on a new Awair Element, the best option seemed to be to replace the whole sensor (a Honeywell HPMA115S0-XXX).</p>
<!-- READMORE -->
<p>Theoretically, you could replace just the 20mm fan inside the sensor, but that would require finding a matching fan, desoldering the old fan, and soldering in the replacement.</p>
<p>I found a replacement sensor from eBay for €16 so the fan replacement didn’t seem worth it, especially considering that the fan would seem to cost as much or even more than the whole sensor assembly.</p>
<h2 id="disassembling-the-awair">Disassembling the Awair</h2>
<p><img src="/_bridgetown/static/IMG_2388-7CD6QSAB.jpg" alt="Awair back cover removed and showing the speaker cable"></p>
<p>Unplug the Awair and remove the four screws at the back to remove the rear plate. Be careful when doing this as the speaker cable needs to be unplugged before the back will come free. There’s enough wire that it might not even be completely necessary to unplug the speaker, but doing so will make the repair a bit easier.</p>
<h2 id="remove-the-old-sensor">Remove the old sensor</h2>
<p><img src="/_bridgetown/static/IMG_2390-KHOWDSCS.jpg" alt="The old PM2.5 sensor inside the Awair"></p>
<p>Lift the sensor and unplug the cable from the sensor side, keeping in mind the orientation of the sensor and cable. Gently peel off the soft pad on top of the sensor and apply it to the new sensor.</p>
<p><strong>Note:</strong> be careful not to get the old and new sensors mixed up along the way 😅</p>
<p><img src="/_bridgetown/static/IMG_2391-TZM5CIGT.jpg" alt="A felt pad on the sensor"></p>
<p>I initially tried replacing it with a felt pad but the rear plate wouldn’t go back into place properly because the felt was too thick and inflexible.</p>
<h2 id="install-the-new-sensor">Install the new sensor</h2>
<p><img src="/_bridgetown/static/IMG_2392-256HHMYR.jpg" alt="The new PM2.5 sensor inside the Awair"></p>
<p>Pop in the cable from the Awair main board to the new sensor and then slot it into place.</p>
<h2 id="reassemble-the-awair">Reassemble the Awair</h2>
<p>Re-attach the speaker wire; the plug is keyed so it <em>should</em> only fit in one orientation. Plug in the USB-C cable and watch your Awair come to life again. Once the Awair has booted up, check that the new sensor is reporting values in the Awair app.</p>
<p>Your Awair should be as good as new!</p>
<p>♻️ Remember to dispose of the broken sensor properly; it’s electronics waste and should be recycled appropriately!</p>DIY Hot Ones2022-02-26T15:05:23+02:002022-02-26T15:05:23+02:00https://www.randomerrata.com/articles/2022/diy-hot-ones/<figure>
<img src="/_bridgetown/static/IMG_1767-BTZD2BLN.jpg" alt="DIY Hot Ones" loading="lazy" data-src="/_bridgetown/static/IMG_1767-BTZD2BLN.jpg" data-blurhash="LQJ7~,n+tlx]DzxYM{xs?ta{Riob" width="1720" height="1290">
</figure>
<p>Da Bomb really is as terrible as everyone says…</p>Cape Town ⛰️☁️2022-02-25T10:58:14+02:002022-02-25T10:58:14+02:00https://www.randomerrata.com/articles/2022/cape-town/<figure>
<img src="/_bridgetown/static/IMG_1734-QIOQ3NRU.jpg" alt="Cape Town ⛰️☁️" loading="lazy" data-src="/_bridgetown/static/IMG_1734-QIOQ3NRU.jpg" data-blurhash="LvGJHif4RjoytpfkWCt6IVogWBoe" width="1720" height="1290">
<figcaption>
<p>View from the Norval Foundation</p>
</figcaption>
</figure>